开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

java8 Stream API及常用方法 java8 St

发表于 2021-08-09

java8 Stream API

Stream 流

  1. Stream(流)是一个来自数据源的元素簇,它可以支持聚合操作。
  2. 数据源:流的数据源,构造流对象的数据源,例如通过一个List来构造Stream对象,这个List就是数据源;
  3. 聚合操作:对Stream对象进行处理后的Stream对象返回指定规则数​​据的操作称为聚合操作,过滤器,映射,限制,排序等都是聚合操作。

创建个实体类

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
kotlin复制代码public class UmsPermission implements Serializable {
private Long id;

@ApiModelProperty(value = "父级权限id")
private Long pid;

@ApiModelProperty(value = "名称")
private String name;

@ApiModelProperty(value = "权限值")
private String value;

@ApiModelProperty(value = "图标")
private String icon;

@ApiModelProperty(value = "权限类型:0->目录;1->菜单;2->按钮(接口绑定权限)")
private Integer type;

@ApiModelProperty(value = "前端资源路径")
private String uri;

@ApiModelProperty(value = "启用状态;0->禁用;1->启用")
private Integer status;

@ApiModelProperty(value = "创建时间")
private Date createTime;

@ApiModelProperty(value = "排序")
private Integer sort;

private static final long serialVersionUID = 1L;

//省略所有getter及setter方法
}

创建流

1
2
3
4
5
6
ini复制代码//创建list
List<UmsPermission> permissionList = new ArrayList();
// 为集合创建串行流对象
Stream<UmsPermission> stream = permissionList.stream();
// 为集合创建并行流对象
Stream<UmsPermission> parallelStream = permissionList.parallelStream();

filter 过滤

filter 主要是做筛选用 filte括号里面结果满足返回true 不满足返回false,返回结果为return true筛选后的结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ini复制代码// 获取权限类型为目录的权限
// permission 为permissionList中每一个对象
// permission.getType() 获取type判断type是否为1 返回数据为type=1的对象
List<UmsPermission> dirList = permissionList.stream()
.filter(permission -> permission.getType() == 0)
.collect(Collectors.toList());

// 第二种写法
List<UmsPermission> dirList = permissionList.stream()
.filter( permission -> {
if (permission.getType() == 0){
return true;
}
return false;
}).collect(Collectors.toList());

map筛选

map为获取map括号里面return 出来的值

1
2
3
4
5
6
7
8
9
scss复制代码// 获取所有权限的id组成的集合
List<Long> idList = permissionList.stream()
.map(permission -> permission.getId())
.collect(Collectors.toList());
//第二种写法
List<Integer> list = permissionList.stream()
.map(permission -> {
return permission.getId();
}).collect(Collectors.toList());

list获取指定数量元素

从Stream中获取指定数量的元素。

1
2
3
4
scss复制代码// 获取前3个权限对象组成的集合
List<UmsPermission> firstFiveList = permissionList.stream()
.limit(3)
.collect(Collectors.toList());

skip跳过指定下标

1
2
3
4
scss复制代码// 跳过前5个元素,返回后面的
List<UmsPermission> skipList = permissionList.stream()
.skip(5)
.collect(Collectors.toList());

count获取总数

1
2
3
4
scss复制代码// count操作:获取所有目录权限的个数
long dirPermissionCount = permissionList.stream()
.filter(permission -> permission.getType() == 0)
.count();

sorted排序

sorted 排序 括号中返回的为 -1 0 1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
scss复制代码// 将所有权限按先目录后菜单再按钮的顺序排序
//默认正序
List<UmsPermission> sortedList = permissionList.stream()
.sorted(Comparator.comparing(UmsPermission::getType)).collect(Collectors.toList());
//倒序 reversed
List<UmsPermission> sortedList = permissionList.stream()
.sorted(Comparator.comparing(UmsPermission::getType).reversed()).collect(Collectors.toList());

//第二种写法 正序
List<UmsPermission> sortedList = permissionList.stream()
//permission1 下一个元素的值 permission2 当前元素的值
.sorted((permission1,permission2)->{
//compareTo对比方法介绍
//permission1.getType() 小于 permission2.getType() 返回 -1 正序
//permission1.getType() 等于 permission2.getType() 返回 0 不排序
//permission1.getType() 大于 permission2.getType() 返回 1 倒序
return permission1.getType().compareTo(permission2.getType());})
.collect(Collectors.toList());

controller方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
scss复制代码 // 对象转Map<Long,UmsPermission> 类型 key:id ,value:对象
Map<Long, UmsPermission> permissionMap = permissionList.stream()
.collect(Collectors.toMap(permission -> permission.getId(), permission -> permission ));

// map中key重复用这个加(oldValue, newValue) -> newValue 的方法
Map<Long, UmsPermission> permissionMap = permissionList.stream()
.collect(Collectors
//(oldValue, newValue) -> newValue 的作用是当出现一样的key值得时候如何取舍其中oldValue代表已存在map中的值,newValue代表新值(当前值),示例中取旧值(toMap key重复会报错) 当前取值为newValue(当前value覆盖原来map中的值)
.toMap(permission -> permission.getId(), permission -> permission ,(oldValue,newValue)-> newValue));

//转换为List<Map<String,Object>>
List<Map<String,Object>> collect = permissionList.stream()
.map(permission -> new BeanMap(permission))
.collect(Collectors.toList());

//转换为Set<Map<String,Object>>
Set<Map<String,Object>> collect = permissionList.stream()
.map(permission -> new BeanMap(permission))
.collect(Collectors.toSet());

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

若依前后台框架,下载运行,若依系统生成代码

发表于 2021-08-09

一、前置工具安装

1
2
3
4
5
6
7
8
scss复制代码jdk 1.8\
redis (缓存数据库)\
mysql\
idea (后端开发工具)\
nodejs (js前端开发平台,本文主要用到包管理工具 npm)\
vscode (前端开发工具)\
git (版本管理工具)
下载 https://gitee.com/y_project/RuoYi-Vue

二、运作方式

在这里插入图片描述

三、后台导入,设置

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

四、后台运行

在这里插入图片描述

五、运行redis, 运行后台,运行前台即可见登录页面

在这里插入图片描述

六、新建测试菜单

在这里插入图片描述

七、使用系统自动生成代码

1、创建商品表

1
2
3
4
5
6
7
c复制代码CREATE TABLE `y_good` (
`id` int(5) NOT NULL AUTO_INCREMENT COMMENT 'id',
`name` varchar(10) NOT NULL COMMENT '名称',
`price` double NOT NULL COMMENT '价格',
`remark` varchar(100) DEFAULT NULL COMMENT '备注',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8

2、导出表生成y_good.sql到桌面

3、使用系统生成代码,编辑完,点生成代码

在这里插入图片描述

4、建立模块ruoyi-test

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

5、前端页面添加

在这里插入图片描述

6、test菜单下添加子菜单

在这里插入图片描述

7、最终成果

在这里插入图片描述

image.png

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Activit7整合Spring、SpringBoot快速精

发表于 2021-08-09

这是我参与8月更文挑战的第5天,活动详情查看:8月更文挑战

@[toc]

Activi7工作流经典实战

=== 图灵: 楼兰 ===

一、Activiti7介绍

​ Activiti是目前使用最为广泛的开源工作流引擎,2010年5月就正是启动了。在了解Activiti之前,我们首先要了解下什么是工作流。

1.1 工作流WorkFlow

​ 关于什么是工作流,有一个官方的定义: 工作流是指一类能够完全自动执行的经营过程,根据一系列现成规则,将文档、信息或任务在不同的执行者之间进行传递和执行。其实说直白一点,就是业务上一个完整的审批流程。例如员工的入职、请假、出差、采购等等、还有一些关键业务如订单申请、合同审核等等,这些过程,都是一个工作流。

​ 对于工作流,传统的处理方式往往需要有人拿着各类的文件,在多个执行部门之间不断的审批。而当我们开始用软件来协助处理这一类审批流程时,就开始出现了工作流系统。工作流系统可以减少大量的线下沟通成本,提高工作效率。

​ 有了工作流系统之后,才开始出现工作流引擎。在没有专门的工作流引擎之前,我们为了实现这样的流程控制,通常的做法都是采用状态字段的方式来跟踪流程的变化情况。例如对一个员工请假请求,我们会定义已申请、组长已审核、部门经理已审核等等这样一些状态,然后通过这些状态来控制不同的业务行为,比如部门经理角色只能看到组长已审核通过的,并且请假天数超过3天的订单等等。

​ 这种实现方式实现起来比较简单,也是软件系统中非常常用的一种方式。但是这种通过状态字段来进行流程控制的方式还是有他的弊端。

​ 一方面:整个流程定义不够清晰。业务流程是分散在各个业务阶段中的,从代码的角度非常难以看到整个流程是如何定义的。

​ 另一方面:当流程发生变更时,这种方式编写的代码就需要做非常大的变更。例如从三级审批要增加为四级审批甚至是协同审批,那各个业务阶段的审批流程都需要随之做大量的变更。

​ 正是出于这些痛点,后面才有了工作流引擎。使用工作流引擎后,整个审批流程可以在同一个地方进行整体设计,并且当审批流程发生变更时,业务程序也可以不用改变。这样业务系统的适应能力就得到了极大提升。

其实引擎的思想无处不在。我们有Drools规则引擎,可以在程序不发生变动的情况下,集中定义业务规则并进行修改。Aviator表达式引擎,可以快速计算某一个表达式的结果。搜索引擎,可以快速进行统一搜索等等。其核心思想都是将业务之间的共性抽取出来,减少业务变动对程序的影响。

1.2 Activiti工作流引擎

​ Activiti正是目前使用最为广泛的开源工作流引擎。Activiti的官网地址是 www.activiti.org 历经6.x和5.x两个大的版本,目前最新的版本是 Activiti Cloud 7.1.0-M11。
在这里插入图片描述

​ 他可以将业务系统中复杂的业务流程抽取出来,使用专门的建模语言BPMN2.0进行定义。业务流程按照预先定义的流程执行,整个实现流程完全由activiti进行管理,从而减少业务系统由于流程变更进行系统改造的工作量,从而减少系统开发维护成本,提高系统的健壮性。所以使用Activiti,重点就是两个步骤,首先使用BPMN定义流程,然后使用Activiti框架实现流程。

1.3 建模语言BPMN

​ 谈到BPMN,首先就要谈BPM。 BPM即Business Process Managemenet,业务流程管理。是一种规范化的构造端到端的业务流程,以持续的提高组织业务效率。在常见的商业管理教育如EMBA、MBA中都包含了BPM的课程。

​ 有了BPM的需求,就出现了BPM软件。他是根据企业中业务环境的变化,推进人与人之间,人与系统之间以及系统与系统之间的整合及调整的经营方法域解决方案的IT工具。通过对企业业务流程的整个生命周期进行建模、自动化、管理监控和优化,使企业成本降低,利润得到提升。BPM软件在企业中应用非常广泛,凡是有业务流程的地方都可以使用BPM进行管理。比如企业人事办公管理、采购流程管理、公文审批流程管理、财务管理等。

​ 而BPMN是Business Process Model And Notation 业务流程模型和符号,就是用来描述业务流程的一种建模标准。BPMN最早由BPMI(BusinessProcess Management Initiative)方案提出。由一整套标准的业务流程建模符号组成。使用BPMN可以快速定义业务流程。

​ BPMN最早在2004年5月发布。2005年9月开始并入OMG(The Object Managemenet Group)组织。OMG于2011年1月发布BPMN2.0的最终版本。BPMN是目前被各大BPM厂商广泛接受的BPM标准。Activiti就是使用BPMN2.0进行流程建模、流程执行管理。

​ 整个BPMN是用一组符号来描述业务流程中发生的各种事件的。BPMN通过在这些符号事件之间连线来描述一个完整的业务流程。

在这里插入图片描述

​ 而对于一个完整的BPMN图形流程,其实最终是通过XML进行描述的。通常,会将BPMN流程最终保存为一个.bpmn的文件,然后可以使用文本编辑器打开进行查看。而图形与xml文件之间,会有专门的软件来进行转换。

​ 关于如何配置一个工作流,在后面的实战过程中我们会接触到。

1.4 Activiti使用步骤

​ 通常使用Activiti时包含以下几个步骤:

  • 部署activiti: Activiti包含一堆Jar包,因此需要把业务系统和Activiti的环境集成在一起进行部署。
  • 定义流程: 使用Activiti的建模工具定义业务流程.bpmn文件。
  • 部署流程定义: 使用Activiti提供的API把流程定义内容存储起来,在Acitivti执行过程汇总可以查询定义的内容。Activiti是通过数据库来存储业务流程的。
  • 启动流程实例: 流程实例也叫ProcessInstance。启动一个流程实例表示开始一次业务流程的运作。例如员工提交请假申请后,就可以开启一个流程实例,从而推动后续的审批等操作。
  • 用户查询待办任务(task):因为现在系统的业务流程都交给了activiti管理,通过activiti就可以查询当前流程执行到哪个步骤了。当前用户需要办理哪些任务也就同样可以由activiti帮我们管理,开发人员不需要自己编写sql语句进行查询了。
  • 用户办理任务:用户查询到自己的待办任务后,就可以办理某个业务,如果这个业务办理完成还需要其他用户办理,就可以由activiti帮我们把工作流程往后面的步骤推动。
  • 流程结束:当任务办理完成没有下一个任务节点后,这个流程实例就执行完成了。

了解这些后,我们来开始进入实战内容。

二、Activiti环境搭建

使用Activiti需要的基本环境包括: JDK 8或以上版本;然后需要一个数据库用来保存流程定义数据,建议mysql 5或以上版本。

2.1 安装插件

开发工具IDEA,在IDEA中需要安装Activiti的流程定义工具插件actiBPM。目前该插件从2014年11月后就没有再更新,对于IDEA版本只支持到2019.1。新版本的IDEA已经无法从插件市场搜索到该插件。安装时,可以到jetBrain的插件市场 plugins.jetbrains.com/ 搜索actiBPM插件,下载到本地后,从本地安装该插件。

在这里插入图片描述

安装完成后,就可以使用这个插件在项目中编辑.bpmn的文件来定义业务流程了。 但是这个文件之前介绍过,他的本质是一个xml文本文件,所以还是需要更多的了解xml的配置方式。

2.2 初始化数据库表

activiti支持多种数据库,详细的版本情况如下:

数据库类型 版本 JDBC连接示例 说明
h2 1.3.168 jdbc:h2:tcp://localhost/activiti 默认配置的数据库
mysql 5.1.21 jdbc:mysql://localhost:3306/activiti?autoReconnect=true 使用 mysql-connector-java 驱动测试
oracle 11.2.0.1.0 jdbc:oracle:thin:@localhost:1521:xe
postgres 8.1 jdbc:postgresql://localhost:5432/activiti
db2 DB2 10.1 using db2jcc4 jdbc:db2://localhost:50000/activiti
mssql 2008 using sqljdbc4 jdbc:sqlserver://localhost:1433/activiti

我们这里选择mysql数据库。接下来按照以下步骤来初始化activiti所需要的数据表。

1- 在mysql中创建一个数据库activiti,将会用来创建activiti相关的表。

1
sql复制代码CREATE DATABASE activiti DEFAULT CHARACTER SET utf8;

然后,activiti需要依赖的业务表有25张,而activiti中提供了工具帮我们生成所需要的表。

2- 创建一个maven工程BasicDemo,在pom.xml中引入以下依赖:

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
xml复制代码<properties>
<slf4j.version>1.6.6</slf4j.version>
<log4j.version>1.2.12</log4j.version>
<activiti.version>7.1.0.M6</activiti.version>
<activiti.cloud.version>7.0.0.Beta1</activiti.cloud.version>
<mysql.version>8.0.20</mysql.version>
</properties>
<dependencies>
<dependency>
<groupId>org.activiti</groupId>
<artifactId>activiti-engine</artifactId>
<version>${activiti.version}</version>
</dependency>
<dependency>
<groupId>org.activiti</groupId>
<artifactId>activiti-spring</artifactId>
<version>${activiti.version}</version>
</dependency>
<!-- bpmn 模型处理 -->
<dependency>
<groupId>org.activiti</groupId>
<artifactId>activiti-bpmn-model</artifactId>
<version>${activiti.version}</version>
</dependency>
<!-- bpmn 转换 -->
<dependency>
<groupId>org.activiti</groupId>
<artifactId>activiti-bpmn-converter</artifactId>
<version>${activiti.version}</version>
</dependency>
<!-- bpmn json数据转换 -->
<dependency>
<groupId>org.activiti</groupId>
<artifactId>activiti-json-converter</artifactId>
<version>${activiti.version}</version>
</dependency>
<!-- bpmn 布局 -->
<dependency>
<groupId>org.activiti</groupId>
<artifactId>activiti-bpmn-layout</artifactId>
<version>${activiti.version}</version>
</dependency>
<!-- activiti 云支持 -->
<dependency>
<groupId>org.activiti.cloud</groupId>
<artifactId>activiti-cloud-services-api</artifactId>
<version>${activiti.cloud.version}</version>
</dependency>
<!-- mysql驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.version}</version>
</dependency>
<!-- mybatis -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.4.5</version>
</dependency>
<!-- 链接池 -->
<dependency>
<groupId>commons-dbcp</groupId>
<artifactId>commons-dbcp</artifactId>
<version>1.4</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
<!-- log start -->
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>${log4j.version}</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${slf4j.version}</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>${slf4j.version}</version>
</dependency>
</dependencies>

3- 添加log4j日志配置

这里采用的是log4j来记录日志,所以需要在resources目录下创建log4j.properties文件来对日志进行配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
properties复制代码# Set root category priority to INFO and its only appender to CONSOLE.
#log4j.rootCategory=INFO, CONSOLE debug info warn error fatal
log4j.rootCategory=debug, CONSOLE, LOGFILE
# Set the enterprise logger category to FATAL and its only appender to CONSOLE.
log4j.logger.org.apache.axis.enterprise=FATAL, CONSOLE
# CONSOLE is set to be a ConsoleAppender using a PatternLayout.
log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender
log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout
log4j.appender.CONSOLE.layout.ConversionPattern=%d{ISO8601} %-6r[%15.15t] %-5p %30.30c %x - %m\n
# LOGFILE is set to be a File appender using a PatternLayout.
log4j.appender.LOGFILE=org.apache.log4j.FileAppender
log4j.appender.LOGFILE.File=f:\act\activiti.log
log4j.appender.LOGFILE.Append=true
log4j.appender.LOGFILE.layout=org.apache.log4j.PatternLayout
log4j.appender.LOGFILE.layout.ConversionPattern=%d{ISO8601} %-6r[%15.15t] %-5p %30.30c %x - %m\n

4- 添加activiti的配置文件

activiti默认就会使用mysql来创建表。创建时需要先创建一个配置文件activiti.cfg.xml,来对数据源信息进行定义。

在resources目录下创建activiti.cfg.xml文件。

注意:这个目录其实就是classpath下的默认位置。这是activiti默认读取的目录和文件。

创建在其他目录下也是可以的,但是就需要在生成时指定文件的目录和名字。

配置文件的基础内容如下: -这里主要是定义几个namespace。

1
2
3
4
5
6
7
8
9
10
11
12
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:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/contex
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd">
</beans>

5- 在activiti.cfg.xml中进行配置

我们可以在activiti.cfg.xml中添加关于数据库的基础配置

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复制代码<?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:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/contex
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd">

<!-- 这里可以使用 链接池 dbcp-->
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource">
<property name="driverClassName" value="com.mysql.cj.jdbc.Driver" />
<property name="url" value="jdbc:mysql://localhost:3306/activiti?serverTimezone=GMT%2B8" />
<property name="username" value="root" />
<property name="password" value="root" />
<property name="maxActive" value="3" />
<property name="maxIdle" value="1" />
</bean>

<bean id="processEngineConfiguration"
class="org.activiti.engine.impl.cfg.StandaloneProcessEngineConfiguration">
<!-- 引用数据源 上面已经设置好了-->
<property name="dataSource" ref="dataSource" />
<!-- activiti数据库表处理策略 -->
<property name="databaseSchemaUpdate" value="true"/>
</bean>
</beans>

注意:1、processEngineConfiguration这个名字最好不要修改。这是activiti读取的默认Bean名字。

2、在processEngineConfiguration中也可以直接配置jdbcDriver、jdbcUrl、jdbcUsername、jdbcPassword几个属性。

3、关于databaseSchemaUpdate这个属性,稍微跟踪一下源码就能看到他的配置方式:

默认是false;表示不创建数据库,只是检查数据库中的表结构,不满足就会抛出异常

create-drop:表示在引擎启动时创建表结构,引擎处理结束时删除表结构。

true:表示创建完整表机构,并在必要时更新表结构。

6- 编写java程序生成表。

创建一个测试类,调用activiti的工具类,直接生成activiti需要的数据库表。代码如下:

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
java复制代码package com.roy;

import org.activiti.engine.ProcessEngine;
import org.activiti.engine.ProcessEngines;
import org.junit.Test;

/**
* @author :楼兰
* @date :Created in 2021/4/7
* @description:
**/

public class TestCreateTable {
/**
* 生成 activiti的数据库表
*/
@Test
public void testCreateDbTable() {
//默认创建方式
ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
//通用的创建方式,指定配置文件名和Bean名称
// ProcessEngineConfiguration processEngineConfiguration = ProcessEngineConfiguration.createProcessEngineConfigurationFromResource("activiti.cfg.xml", "processEngineConfiguration");
// ProcessEngine processEngine1 = processEngineConfiguration.buildProcessEngine();
System.out.println(processEngine);
}
}

注意:从这个代码就能看出我们之前那些默认配置的作用。ProcessEngines.getDefaultProcessEngine()这行代码默认就会去读取classpath:下的activiti.cfg.xml和activiti-context.xml两个配置文件。并且从spring容器中加载名为processEngineConfiguration的Bean。

执行这个脚本就会完成mysql的表结构创建。如果执行正常,可以看到执行了一大堆的sql语句,最终打印出一行日志

1
css复制代码org.activiti.engine.impl.ProcessEngineImpl@77307458

这就表示引擎创建成功了。同时在mysql中可以看到activiti用到的25张表。

在这里插入图片描述

这些表机构通常也可以导出成sql文件,然后直接进行移植。但是考虑到不同版本可能会有微调,所以通常不建议以sql文件的方式移植。

2.3 表结构解读

​ 从这些刚才创建的表中可以看到,activiti的表都以act_开头。第二个部分表示表的用途。用途也和服务的API对应。

ACT_RE :’RE’表示 repository。 这个前缀的表包含了流程定义和流程静态资源 (图片,规则,等等)。
ACT_RU:’RU’表示 runtime。 这些运行时的表,包含流程实例,任务,变量,异步任务,等运行中的数据。 Activiti 只在流程实例执行过程中保存这些数据, 在流程结束时就会删除这些记录。 这样运行时表可以一直很小速度很快。
ACT_HI:’HI’表示 history。 这些表包含历史数据,比如历史流程实例, 变量,任务等等。
ACT_GE : GE 表示 general。 通用数据, 用于不同场景下

​ 完整的数据库表作用如下:

表分类 表名 解释
一般数据
[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_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] 运行时变量表

2.4 Activiti核心类

​ 当拿到ProcessEngine之后,我们可以简单的看一下他的方法

在这里插入图片描述

这几个service就是activiti最为核心的几个服务实现类。围绕activiti的核心业务功能大都通过这几个service来组成。

service名称 service作用
RepositoryService activiti的资源管理类
RuntimeService activiti的流程运行管理类
TaskService activiti的任务管理类
HistoryService activiti的历史管理类
ManagerService activiti的引擎管理类

简单介绍:

RepositoryService

是activiti的资源管理类,提供了管理和控制流程发布包和流程定义的操作。使用工作流建模工具设计的业务流程图需要使用此service将流程定义文件的内容部署到计算机。

除了部署流程定义以外还可以:查询引擎中的发布包和流程定义。

暂停或激活发布包,对应全部和特定流程定义。 暂停意味着它们不能再执行任何操作了,激活是对应的反向操作。获得多种资源,像是包含在发布包里的文件, 或引擎自动生成的流程图。

获得流程定义的pojo版本, 可以用来通过java解析流程,而不必通过xml。

RuntimeService

Activiti的流程运行管理类。可以从这个服务类中获取很多关于流程执行相关的信息

TaskService

Activiti的任务管理类。可以从这个类中获取任务的信息。

HistoryService

Activiti的历史管理类,可以查询历史信息,执行流程时,引擎会保存很多数据(根据配置),比如流程实例启动时间,任务的参与者, 完成任务的时间,每个流程实例的执行路径,等等。 这个服务主要通过查询功能来获得这些数据。

ManagementService

Activiti的引擎管理类,提供了对 Activiti 流程引擎的管理和维护功能,这些功能不在工作流驱动的应用程序中使用,主要用于 Activiti 系统的日常维护。

三、Activiti入门

​ 在这一章,我们就来创建一个Activiti工作流,并启动这个工作流。了解Activiti的基础开发流程。

​ 创建Activiti工作流的主要步骤包含以下几步:

  1. 定义流程。按照BPMN的规范,使用流程定义工具,将整个流程描述出来
  2. 部署流程。把画好的BPMN流程定义文件加载到数据库中,生成相关的表数据
  3. 启动流程。使用java代码来操作数据库表中的内容。

3.1 流程符号详解

​ 接下来我们来了解下在流程设计中常见的符号。BPMN2.0的基本符号主要包含以下几类:

  • 事件 Event

在这里插入图片描述

事件是驱动工作流发展的核心对象,在工作流的流程定制过程中会经常看到。

  • 活动 Activity

在这里插入图片描述

活动是工作或任务的一个通用术语。一个活动可以是一个任务,也可以是当前流程的子处理流程。并且,活动会有不同的类型。例如Activiti中定义了UserTask,ScriptTask,ServiceTask,MailTask等等多种类型。这些活动是构成整个业务流程的主体。

  • 网关 GateWay

在这里插入图片描述

网关是用来处理角色的,他决定了工作流的业务走向。有几种常用的网关需要了解一下:

  • 排他网关

只有一条路径会被选择。流程执行到该网关时,会按照输出流的顺序逐个计算,当条件的结果为true时,继续执行当前网关的输出流。

​ 如果有多条线路计算结构都是true,则会执行第一个值为true的路线。如果所有网关计算结果都没有true,引擎会抛出异常。

​ 排他网关需要和条件顺序流结合使用,default属性指定默认顺序流,当所有的条件不满足时会执行默认顺序流。

  • 并行网关

所有路径会被同时选择。

并行执行所有输出顺序流,为每一条顺序流创建一个并行执行路线。最终,在所有的执行路线都执行完后才继续向下执行。

  • 包容网关

可以同时执行多条路线。相当于是排他网关和并行网关的结合。

可以在网关上设置条件,计算每条路线上的表达式。当表达式计算结果为true时,创建一个并行线路并继续执行。最终,当所有需要执行的线路都执行完成后才继续向下执行。

  • 事件网关

专门为中间捕获事件而设置。允许设置多个输出流指向多个不同的中间捕获事件。当流程执行到事件网关后,流程出于等待状态,需要等待抛出对应的事件才能将等待状态转换为活动状态。

  • 流向 Flow

在这里插入图片描述

流就是连接两个流程节点的连线,代表了流程之间的关联关系。

注意:Activiti的BPMN符号是在标准符号基础上做了一定的扩展。你可以新建一个BPMN文件,然后对照actBPM工具提供的工作流图形来理解。

3.2 定制一个简单的请假流程

首先在resources目录下创建bpmn目录,然后在目录下创建一个Bomn文件,起名Leave,表示是一个请假流程。

然后我们在流程图中拖拽出下图的流程图

在这里插入图片描述

这里注意每个模块都可以选中后在左侧设置他的属性。

在这里插入图片描述

这里,Assignee属性表示是这个任务的负责人。这里我们给 创建出差申请 设置负责人 worker;部门经理审批 设置负责人 manager;财务审批 设置负责人 financer。

设置完成后,记得点击一下流程的空白页,在左侧设置整个流程的属性。

在这里插入图片描述

这样,我们整个员工出差申请的流程就定义完成了。之前介绍过,这个文件实际上是一个xml文件,所以,这个文件是可以用文本编辑器直接打开的。整个文件大致是这样

在这里插入图片描述

其中根节点是definitions节点。在这个节点中,可以定义多个工作流程process节点。这也就意味着可以在一个图中定义多个工作流,但是通常在使用过程中建议一个文件只包含一个流程定义,这样可以简化维护难度。definitions节点中,xmlns和tagetNamespace两个属性是必须要包含的。

然后在文件中,包含了以标签描述的流程定义部分以及以<bpmndi:BPMNDiagram >标签描述的流程布局定义部分。分别用来定义工作流程以及流程图的布局信息。

文件关闭后,重新打开,可能会出现中文乱码的问题。这是因为IDEA的字符集问题。此时,需要修改IDEA的配置文件。 在IDEA中选Help -> Edit Custom VM Options 菜单,在打开的文件最后加上一行-Dfile.encoding=UTF-8配置,然后重启IDEA即可。

3.3 部署请假流程

接下来,需要将设计器中定义的流程部署到activiti的数据库中。activiti提供了api将流程定义的bpmn和png文件部署到activiti中。

要获取png图片文件,可以直接截图,也可以使用IDEA导出。导出时,先将文件修改为Leave.bpmn.xml,然后右键该文件,选择Diagrams -> Shwo BPMN 2.0 Diagrams,可以打开图片工具,然后选择上方的导出按钮,就可以导出一个png文件。

在这里插入图片描述

部署流程时,可以分别上传bpmn文件和png文件,也可以将两个文件打成zip压缩包一起上传。

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
java复制代码public class ActivitiDemo {

/**
* 部署流程定义 文件上传方式
*/
@Test
public void testDeployment(){
// 1、创建ProcessEngine
ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
// 2、得到RepositoryService实例
RepositoryService repositoryService = processEngine.getRepositoryService();
// 3、使用RepositoryService进行部署
Deployment deployment = repositoryService.createDeployment()
.addClasspathResource("bpmn/Leave.bpmn") // 添加bpmn资源
//png资源命名是有规范的。Leave.[key].[png|jpg|gif|svg] 或者Leave.[png|jpg|gif|svg]
.addClasspathResource("bpmn/Leave.myLeave.png") // 添加png资源
.name("请假申请流程")
.deploy();
// 4、输出部署信息
System.out.println("流程部署id:" + deployment.getId());
System.out.println("流程部署名称:" + deployment.getName());
}

/**
* zip压缩文件上传方式
*/
@Test
public void deployProcessByZip() {
// 定义zip输入流
InputStream inputStream = this
.getClass()
.getClassLoader()
.getResourceAsStream(
"bpmn/Leave.zip");
ZipInputStream zipInputStream = new ZipInputStream(inputStream);
// 获取repositoryService
ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
RepositoryService repositoryService = processEngine
.getRepositoryService();
// 流程部署
Deployment deployment = repositoryService.createDeployment()
.addZipInputStream(zipInputStream)
.deploy();
System.out.println("流程部署id:" + deployment.getId());
System.out.println("流程部署名称:" + deployment.getName());
}
}

这个过程中,最重要的就是要去找repositoryService。执行完成后可以在日志中看到部署的情况:

1
2
bash复制代码流程部署id:1
流程部署名称:请假申请流程

并且,从日志中可以分析出整个部署过程操作了三张数据表:

  • act_re_deployment 流程定义部署表,每部署一次增加一条记录
  • act_re_procdef 流程定义表,部署每个新的流程定义都会在这张表中增加一条记录。记录中的key就是流程定义中最为重要的字段。
  • act_ge_bytearray 流程资源表 ,每个流程定义对应两个资源记录,bpmn和png。

一次部署可以部署多个流程定义 即act_re_deployment和act_re_procdef中的数据其实是一对多的。但是在实际开发中,建议一次部署只不是一个流程。

3.4 启动流程实例

一个业务流程部署到activiti后,就可以使用了。例如这个出差申请的流程部署完成后,就可以启动一个流程进行一次出差申请了。流程的执行过程主要通过RuntimeService服务来管理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码 /**
* 启动流程实例
*/
@Test
public void testStartProcess(){
// 1、创建ProcessEngine
ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
// 2、获取RunTimeService
RuntimeService runtimeService = processEngine.getRuntimeService();
// 3、根据流程定义Id启动流程
ProcessInstance processInstance = runtimeService
.startProcessInstanceByKey("myLeave");
// 输出内容
System.out.println("流程定义id:" + processInstance.getProcessDefinitionId());
System.out.println("流程实例id:" + processInstance.getId());
System.out.println("当前活动Id:" + processInstance.getActivityId());
}

执行结果可以看到流程实例的情况:

1
2
3
bash复制代码流程定义id:myLeave:1:4
流程实例id:2501
当前活动Id:null

继续查看日志,可以看到这个过程中涉及到的数据表:

  • act_hi_actinst 流程实例执行历史
  • act_hi_identitylink 流程的参与用户历史信息
  • act_hi_procinst 流程实例历史信息
  • act_hi_taskinst 流程任务历史信息
  • act_ru_execution 流程执行信息
  • act_ru_identitylink 流程的参与用户信息
  • act_ru_task 任务信息

3.5 任务查询

流程启动后,任务的负责人就可以查询自己当前需要处理的待办任务了。任务相关的服务都是由TaskService管理。

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
csharp复制代码/**
* 查询当前个人待执行的任务
*/
@Test
public void testFindPersonalTaskList() {
// 任务负责人
String assignee = "worker";
ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
// 创建TaskService
TaskService taskService = processEngine.getTaskService();
// 根据流程key 和 任务负责人 查询任务
List<Task> list = taskService.createTaskQuery()
.processDefinitionKey("myLeave") //流程Key
.taskAssignee(assignee)//只查询该任务负责人的任务
.list();

for (Task task : list) {

System.out.println("流程实例id:" + task.getProcessInstanceId());
System.out.println("任务id:" + task.getId());
System.out.println("任务负责人:" + task.getAssignee());
System.out.println("任务名称:" + task.getName());

}
}

执行完成后可以看到当前流程的任务列表

1
2
3
4
bash复制代码流程实例id:2501
任务id:2505
任务负责人:worker
任务名称:创建请假申请

当前请假流程启动后,就该等待worker用户提交请假申请了。实际中的请假申请流程应该是从worker提交请假申请开始,但是在activiti工作流中,都是从starter事件开始,这个关系要理清楚。

3.6 流程任务处理

任务负责人查询到代办任务后,可以选择任务进行处理,完成任务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码// 完成任务
@Test
public void completTask(){
// 获取引擎
ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
// 获取taskService
TaskService taskService = processEngine.getTaskService();

// 根据流程key 和 任务的负责人 查询任务
// 返回一个任务对象
Task task = taskService.createTaskQuery()
.processDefinitionKey("myLeave") //流程Key
.taskAssignee("worker") //要查询的负责人
.singleResult();

// 完成任务,参数:任务id
taskService.complete(task.getId());
}

这个任务完成后,这一个请假流程就推动到了下一个步骤,部门经理审批了。后续可以用不同的用户来推动流程结束。

其实在完成审批任务的过程中,可以针对这个taskId,进行其他一些补充操作。例如添加Comment,添加附件,添加子任务,添加候选负责人等等。具体可以看下taskService的API。

3.7 流程信息查询

这一步可以查询流程相关信息,包含流程定义,流程部署,流程版本。

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
java复制代码    /**
* 查询流程定义
*/
@Test
public void queryProcessDefinition(){
// 获取引擎
ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
// repositoryService
RepositoryService repositoryService = processEngine.getRepositoryService();
// 得到ProcessDefinitionQuery 对象
ProcessDefinitionQuery processDefinitionQuery = repositoryService.createProcessDefinitionQuery();
// 查询出当前所有的流程定义
// 条件:processDefinitionKey =evection
// orderByProcessDefinitionVersion 按照版本排序
// desc倒叙
// list 返回集合
List<ProcessDefinition> definitionList = processDefinitionQuery.processDefinitionKey("myLeave")
.orderByProcessDefinitionVersion()
.desc()
.list();
// 输出流程定义信息
for (ProcessDefinition processDefinition : definitionList) {
System.out.println("流程定义 id="+processDefinition.getId());
System.out.println("流程定义 name="+processDefinition.getName());
System.out.println("流程定义 key="+processDefinition.getKey());
System.out.println("流程定义 Version="+processDefinition.getVersion());
System.out.println("流程部署ID ="+processDefinition.getDeploymentId());
}

}

执行后可以看到查询结果

1
2
3
4
5
ini复制代码流程定义 id=myLeave:1:4
流程定义 name=员工请假审批流程
流程定义 key=myLeave
流程定义 Version=1
流程部署ID =1

3.8 删除流程

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码public void deleteDeployment() {
// 流程部署id
String deploymentId = "1";

ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
// 通过流程引擎获取repositoryService
RepositoryService repositoryService = processEngine
.getRepositoryService();
//删除流程定义,如果该流程定义已有流程实例启动则删除时出错
repositoryService.deleteDeployment(deploymentId);
//设置true 级联删除流程定义,即使该流程有流程实例启动也可以删除,设置为false非级连删除方式
//repositoryService.deleteDeployment(deploymentId, true);
}

注:

1、这里只删除了流程定义,不会删除历史表信息

2、删除任务时,可以选择传入一个boolean型的变量cascade ,表示是否级联删除。默认是false,表示普通删除。

如果该流程下存在已经运行的流程,使用普通删除会报错,而级联删除可以将流程及相关记录全部删除。删除没有完成的流程节点后,就可以完全删除流程定义信息了。

项目开发中,级联删除操作一般只开放给管理员使用。

3.9 流程资源下载

在流程执行过程中,可以上传流程资源文件。我们之前在部署流程时,已经将bpmn和描述bpmn的png图片都上传了,并且在流程执行过程中,也可以上传资源文件。如果其他用户想要查看这些资源文件,可以从数据库中把资源文件下载下来。

但是文件是以Blob的方式存在数据库中的,要获取Blob文件,可以使用JDBC来处理。也可以使用activiti提供的api来辅助实现。我们这里采用activiti的方式来实现。

首先引入commons-io依赖

1
2
3
4
5
xml复制代码<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.6</version>
</dependency>

然后,就可以通过流程定义对象来获取流程资源。这里获取我们之前上传的bpmn和png文件

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
java复制代码import org.apache.commons.io.IOUtils;

@Test
public void deleteDeployment(){
// 获取引擎
ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
// 获取repositoryService
RepositoryService repositoryService = processEngine.getRepositoryService();
// 根据部署id 删除部署信息,如果想要级联删除,可以添加第二个参数,true
repositoryService.deleteDeployment("1");
}

public void queryBpmnFile() throws IOException {
// 1、得到引擎
ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
// 2、获取repositoryService
RepositoryService repositoryService = processEngine.getRepositoryService();
// 3、得到查询器:ProcessDefinitionQuery,设置查询条件,得到想要的流程定义
ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery()
.processDefinitionKey("myLeave")
.singleResult();
// 4、通过流程定义信息,得到部署ID
String deploymentId = processDefinition.getDeploymentId();
// 5、通过repositoryService的方法,实现读取图片信息和bpmn信息
// png图片的流
InputStream pngInput = repositoryService.getResourceAsStream(deploymentId, processDefinition.getDiagramResourceName());
// bpmn文件的流
InputStream bpmnInput = repositoryService.getResourceAsStream(deploymentId, processDefinition.getResourceName());
// 6、构造OutputStream流
File file_png = new File("d:/myLeave.png");
File file_bpmn = new File("d:/myLeave.bpmn");
FileOutputStream bpmnOut = new FileOutputStream(file_bpmn);
FileOutputStream pngOut = new FileOutputStream(file_png);
// 7、输入流,输出流的转换
IOUtils.copy(pngInput,pngOut);
IOUtils.copy(bpmnInput,bpmnOut);
// 8、关闭流
pngOut.close();
bpmnOut.close();
pngInput.close();
bpmnInput.close();
}

注: 在获取资源文件名时,png图片资源的文件名是processDefinition.getDiagramResourceName(),他来自于ACT_RE_PROCDEF表中的DGRM_RESOURCE_NAME字段。这个字段的值是在部署流程时根据文件名后缀判断出来的。 支持的格式为[ResourceName].[key].[png|jpg|gif|svg]或者[ResourceName].[png|jpg|gif|svg]

而bpmn文件的文件名是processDefinition.getResourceName(),他来自于ACT_RE_PROCDEF表中的RESOURCE_NAME字段。

3.10 流程历史信息查看

流程的历史信息都保存在activiti的act_hi_*相关的表中,我们可以查询流程执行的历史信息。这里需要通过HistoryService来查看相关的历史记录。

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
java复制代码/**
* 查看历史信息
*/
@Test
public void findHistoryInfo(){
// 获取引擎
ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
// 获取HistoryService
HistoryService historyService = processEngine.getHistoryService();
// 获取 actinst表的查询对象
HistoricActivityInstanceQuery instanceQuery = historyService.createHistoricActivityInstanceQuery();
// 查询 actinst表,条件:根据 InstanceId 查询,查询一个流程的所有历史信息
instanceQuery.processInstanceId("25001");
// 查询 actinst表,条件:根据 DefinitionId 查询,查询一种流程的所有历史信息
// instanceQuery.processDefinitionId("myLeave:1:22504");
// 增加排序操作,orderByHistoricActivityInstanceStartTime 根据开始时间排序 asc 升序
instanceQuery.orderByHistoricActivityInstanceStartTime().asc();
// 查询所有内容
List<HistoricActivityInstance> activityInstanceList = instanceQuery.list();
// 输出
for (HistoricActivityInstance hi : activityInstanceList) {
System.out.println(hi.getActivityId());
System.out.println(hi.getActivityName());
System.out.println(hi.getProcessDefinitionId());
System.out.println(hi.getProcessInstanceId());
System.out.println("<==========================>");
}
}

这样可以查询到之前的步骤处理结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
diff复制代码_2
StartEvent
myLeave:1:22504
25001
<==========================>
_3
创建请假申请
myLeave:1:22504
25001
<==========================>
_4
部门经理审批
myLeave:1:22504
25001
<==========================>

注:1、关于流程历史信息,要注意,在删除流程时,如果是采取级联删除的方式,那这个历史信息也会随着一起删除。而普通删除方式不会删除历史信息。

2、历史信息有不同的种类,具体可以通过historyService构建不同类型的Query对象来获取结果。

3.11 篇章总结

​ 通过这一章的内容,我们已经完成了一个工作流的基础流程,也能对activiti的工作机制有个大致的了解。

​ activiti的强大之处在于,他围绕BPMN2.0构建的工作流程定义,提供了一系列完整的后台工作流功能。这些后台功能可以构成一个稳定的后台程序,针对不同的业务流程,只需要提供不同的BPMN定义文件,而不需要修改后台程度代码。并且,当业务流程发生变动时,也只需要修改BPMN文件中的流程定义,相应的后台代码基本不需要动。

​ 但是也要看到,activiti只提供了后台功能,并没有配套的前端整合。并且,当需要对某一个任务或者某一个工作流做一些细致性的操作时,还是需要传入一些特定的业务参数,而这些业务参数,还是需要有个系统来进行整体的处理的。所以,activiti是一个强大的工作流引擎,但是距离一个完整的工作流系统还是有点差距的。

​ 另外,在学习activiti时,数据库中的25张表也是非常关键的地方,这些最终保存的数据中包含了工作流运行过程中的所有数据。在实际使用中,我们基本不可能完全搞明白这25张表的数据内容,但是对于一些关键的操作数据还是需要了解下的,这是我们以后掌握整个工作流引擎运行状态的重要依据。

​ 在我们对Activiti有了大致的理解后,接下来将深入一些Activiti的进阶功能。

四、Activiti进阶

4.1 流程定义与流程实例

流程定义 ProcessDefinition 和流程实例 ProcessInstance是Activiti中非常重要的两个概念。他们的关系其实类似于JAVA中类和对象的概念。

流程定义ProcessDefinition是以BPMN文件定义的一个工作流程,是一组工作规范。例如我们之前定义的请假流程。流程实例ProcessInstance则是指一个具体的业务流程。例如某个员工发起一次请假,就会实例化一个请假的流程实例,并且每个不同的流程实例之间是互不影响的。

在后台的表结构中,有很多张表都包含了流程定义ProcessDefinetion和流程实例ProcessInstance的字段。流程定义的字段通常是PROC_DEF_ID,而流程实例的字段通常是PROC_INST_ID。

4.1.1 启动流程实例时,添加Businesskey

在之前的简单案例中,我们启动一个流程实例的关键代码其实就是这一行。

1
java复制代码ProcessInstance processInstance = runtimeService              .startProcessInstanceByKey("myLeave");

当我们去查看下startProcessInstanceByKey这个方法时,会看到这个方法有好几个重载的实现方法,可以传一些不同的参数。其中几个重要的参数包括

  • String processDefinitionKey:流程定义的唯一键 不能为空
  • String businessKey:每个线程实例上下文中关联的唯一键。这个也是我们这一章节要介绍的重点。
  • Map<String,Object> variables:在线程实例中传递的流程变量。这个流程变量可以在整个流程实例中使用,后面会介绍到。
  • String tenantId:租户ID,这是Activiti的多租户设计。相当于每个租户可以上来获取一个相对独立的运行环境。

这一章节我们来介绍这个businessKey,业务关键字。这是Activiti提供的一个非常重要的便利,用来将activiti的工作流程与实际业务进行关联。

例如,当我们需要对一个业务订单进行审批时,订单的详细信息并不在activiti的数据当中,但是在审批时确实需要查看这些订单的详细信息。这个时候,就可以用这个businessKey来关联订单ID,这样在业务系统中,就可以通过这个订单ID去关联订单详细信息,审批人员就可以快速拿来进行参考。

进行实际业务整合时,这个businessKey可以根据业务场景,设计成不同的数据格式,比如关键信息逗号拼接,甚至是json都可以,唯一需要注意的是这个字段的数据库长度设计是255,不要超出了数据库的长度限制。

接下来,我们看看如何在流程实例执行过程中获取这个业务关键字:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
java复制代码@Test
public void queryProcessInstance() {
// 流程定义key
String processDefinitionKey = "myLeave";
ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
// 获取RunTimeService
RuntimeService runtimeService = processEngine.getRuntimeService();
List<ProcessInstance> list = runtimeService
.createProcessInstanceQuery()
.processDefinitionKey(processDefinitionKey)//
.list();

for (ProcessInstance processInstance : list) {
System.out.println("----------------------------");
System.out.println("流程实例id:"
+ processInstance.getProcessInstanceId());
System.out.println("所属流程定义id:"
+ processInstance.getProcessDefinitionId());
System.out.println("是否执行完成:" + processInstance.isEnded());
System.out.println("是否暂停:" + processInstance.isSuspended());
System.out.println("当前活动标识:" + processInstance.getActivityId());
System.out.println("业务关键字:"+processInstance.getBusinessKey());
}
}

通过最后面的一行processInstance.getBusinessKey()就能获取到当前流程实例中的业务关键字。在数据库中,act_ru_execution表中的BUSINESS_KEY字段就是用来保存这个业务关键字的。

4.1.2 挂起、激活流程实例

​ 之前我们已经测试了如何删除一个流程,有很多时候,我们只是需要暂时停止一个流程,过一段时间就要恢复。例如月底不接受报销审批流程,年底不接受借贷审批流程,或者非工作日不接受售后报销流程等,这个时候,就可以将流程进行挂起操作。挂起后的流程就不会再继续执行。

​ 在挂起流程时,有两种操作方式。

一种是将整个流程定义Process Definition挂起,这样,这个流程定义下的所有流程实例都将挂起,无法继续执行

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
java复制代码/**
* 全部流程实例挂起与激活
*/
@Test
public void SuspendAllProcessInstance(){
// 获取processEngine
ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
// 获取repositoryService
RepositoryService repositoryService = processEngine.getRepositoryService();
// 查询流程定义的对象
ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery().
processDefinitionKey("myEvection").
singleResult();
// 得到当前流程定义的实例是否都为暂停状态
boolean suspended = processDefinition.isSuspended();
// 流程定义id
String processDefinitionId = processDefinition.getId();
// 判断是否为暂停
if(suspended){
// 如果是暂停,可以执行激活操作 ,参数1 :流程定义id ,参数2:是否激活,参数3:激活时间
repositoryService.activateProcessDefinitionById(processDefinitionId,
true,
null
);
System.out.println("流程定义:"+processDefinitionId+",已激活");
}else{
// 如果是激活状态,可以暂停,参数1 :流程定义id ,参数2:是否暂停,参数3:暂停时间
repositoryService.suspendProcessDefinitionById(processDefinitionId,
true,
null);
System.out.println("流程定义:"+processDefinitionId+",已挂起");
}

}

​ 另一种方式是将某一个具体的流程实例挂起。例如对某一个有问题的请假申请进行挂起操作,数据调整完成后再进行激活。继续执行挂起状态的流程将会抛出异常

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
java复制代码/**
* 单个流程实例挂起与激活
*/
@Test
public void SuspendSingleProcessInstance(){
// 获取processEngine
ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
// RuntimeService
RuntimeService runtimeService = processEngine.getRuntimeService();
// 查询流程定义的对象
ProcessInstance processInstance = runtimeService.
createProcessInstanceQuery().
processInstanceId("15001").
singleResult();
// 得到当前流程定义的实例是否都为暂停状态
boolean suspended = processInstance.isSuspended();
// 流程定义id
String processInstanceId = processInstance.getId();
// 判断是否为暂停
if(suspended){
// 如果是暂停,可以执行激活操作 ,参数:流程定义id
runtimeService.activateProcessInstanceById(processInstanceId);
System.out.println("流程定义:"+processDefinitionId+",已激活");
}else{
// 如果是激活状态,可以暂停,参数:流程定义id
runtimeService.suspendProcessInstanceById( processInstanceId);
System.out.println("流程定义:"+processDefinitionId+",已挂起");
}
}

/**
* 测试完成个人任务
*/
@Test
public void completTask(){
// 获取引擎
ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
// 获取操作任务的服务 TaskService
TaskService taskService = processEngine.getTaskService();
// 完成任务,参数:流程实例id,完成zhangsan的任务
Task task = taskService.createTaskQuery()
.processInstanceId("15001")
.taskAssignee("rose")
.singleResult();

System.out.println("流程实例id="+task.getProcessInstanceId());
System.out.println("任务Id="+task.getId());
System.out.println("任务负责人="+task.getAssignee());
System.out.println("任务名称="+task.getName());
taskService.complete(task.getId());
}

4.2 流程变量

流程变量也是Activiti中非常重要的角色。我们之前定义的请假流程并没有用到流程变量,每个步骤都是非常固定的,但是,当我们需要实现一些复杂的业务流程,比如请假3天以内由部门经理审批,3天以上需要增加总经理审批这样的流程时,就需要用到流程变量了。

注:这个流程变量和之前介绍的业务关键字其实是有些相似的,都可以携带业务信息。并且也都可以通过activiti的api查询出来。但是通常在使用过程中,应该尽量减少流程变量中的业务信息,这样能够减少业务代码对activiti工作流的代码侵入。

在上一章节介绍到,流程变量的类型是Map<String,Object>。所以,流程变量比业务关键字要强大很多。变量值不仅仅是字符串,也可以是POJO对象。但是当需要将一个POJO对象放入流程变量时,要注意这个对象必须要实现序列化接口serializable。

4.2.1 流程变量的作用域

变量的作用域可以设置为Global和Local两种。

  • Global变量

这个是流程变量的默认作用域,表示是一个完整的流程实例。 Global变量中变量名不能重复。如果设置了相同的变量名,后面设置的值会直接覆盖前面设置的变量值。

  • Local 变量

Local变量的作用域只针对一个任务或一个执行实例的范围,没有流程实例大。Local变量由于作用在不同的任务或不同的执行实例中,所以不同变量的作用域是互不影响的,变量名可以相同。Local变量名也可以和Global变量名相同,不会有影响。

4.2.2 使用流程变量

​ 定义好流程变量后,就可以在整个流程定义中使用这些流程变量了。例如可以在某些任务属性如assignee上使用assignee,或者在某些连线上使用{assignee},或者在某些连线上使用assignee,或者在某些连线上使用{day<3}。

​ Activiti中可以使用UEL表达式来使用这些流程变量。UEL表达式可以直接获取一个变量的值,可以计算一个Boolean结果的表达式,还可以直接使用某些对象的属性。例如对于之前创建的请假流程,如果要实现3天以内部门经理审核,3天以上增加总经理审核,可以做如下调整:

在这里插入图片描述

1)、出差天数大于等于3连线条件

在这里插入图片描述

在这里插入图片描述

也可以使用对象参数命名,如evection.num:

在这里插入图片描述

在这里插入图片描述

2)、出差天数小于3连线条件

在这里插入图片描述

在这里插入图片描述

也可以使用对象参数命名,如:

在这里插入图片描述

在这里插入图片描述

4.2.3 设置Global流程变量

在流程定义中使用到了流程变量,就需要在后台JAVA代码中设置对应的流程变量。 实际上在流程执行的很多过程中都可以设计自流程变量。

1) 启动流程时设置变量

在启动流程实例时设置流程变量,这时流程变量的作用域是整个流程实例。相当于是Global作用域。核心代码:

1
java复制代码ProcessInstance processInstance = runtimeService.startProcessInstanceByKey(key, map);
2) 任务办理时设置变量

在完成任务时设置流程变量,该流程变量只有在该任务完成后其它结点才可使用该变量,它的作用域是整个流程实例,如果设置的流程变量的key在流程实例中已存在相同的名字则后设置的变量替换前边设置的变量。核心代码:

1
java复制代码taskService.complete(task.getId(),map);

注意:这种方式设置流程变量,如果当前执行的任务ID不存在,则会抛出异常,流程变量也会设置失败。

3) 通过当前流程实例设置

通过流程实例id设置全局变量,该流程实例必须未执行完成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码  @Test
public void setGlobalVariableByExecutionId(){
// 当前流程实例执行 id,通常设置为当前执行的流程实例
String executionId="2601";
// 获取processEngine
ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
// 获取RuntimeService
RuntimeService runtimeService = processEngine.getRuntimeService();
// 创建出差pojo对象
Evection evection = new Evection();
// 设置天数
evection.setNum(3d);
// 通过流程实例 id设置流程变量
runtimeService.setVariable(executionId, "myLeave", evection);
// 一次设置多个值
// runtimeService.setVariables(executionId, variables)
}

注意:ececutionId必须是当前未完成的流程实例的执行ID。通常此ID设置流程实例的ID。流程变量设计完成后,也可以通过runtimeService.getVariable()获取流程变量

4) 通过当前任务设置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码@Test
public void setGlobalVariableByTaskId(){

//当前待办任务id
String taskId="1404";
// 获取processEngine
ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
TaskService taskService = processEngine.getTaskService();
Evection evection = new Evection();
evection.setNum(3);
//通过任务设置流程变量
taskService.setVariable(taskId, "evection", evection);
//一次设置多个值
//taskService.setVariables(taskId, variables)
}

注: 任务id必须是当前待办任务id,act_ru_task中存在。如果该任务已结束,会报错。也可以通过taskService.getVariable()获取流程变量。

注意事项

1、 如果UEL表达式中流程变量名不存在则报错。

2、 如果UEL表达式中流程变量值为空NULL,流程不按UEL表达式去执行,而流程结束 。

3、 如果UEL表达式都不符合条件,流程结束

4、 如果连线不设置条件,会走flow序号小的那条线

5、设置流程变量会在当前执行流程变量表act_ru_variable中插入记录,同时也会在历史流量变量表act_hi_varinst中也插入记录。

4.2.4 设置Local流程变量

local流程变量同样可以有多个设置的地方。

1) 任务办理时设置

任务办理时设置local流程变量,当前运行的流程实例只能在该任务结束前使用,任务结束该变量无法在当前流程实例使用,可以通过查询历史任务查询。关键代码:

1
2
3
4
java复制代码//  设置local变量,作用域为该任务
taskService.setVariablesLocal(taskId, variables);
// 完成任务
taskService.complete(taskId);
2) 通过当前任务设置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码@Test
public void setLocalVariableByTaskId(){
// 当前待办任务id
String taskId="1404";
// 获取processEngine
ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
TaskService taskService = processEngine.getTaskService();
Evection evection = new Evection ();
evection.setNum(3d);
// 通过任务设置流程变量
taskService.setVariableLocal(taskId, "evection", evection);
// 一次设置多个值
//taskService.setVariablesLocal(taskId, variables)
}

注: 任务ID必须是当前待办任务id,要在act_ru_task中存在

4.3 网关

网关是用来控制流程流向的重要组件,通常都会要结合流程变量来使用。

4.3.1 排他网关ExclusiveGateway

排他网关,用来在流程中实现决策。 当流程执行到这个网关,所有分支都会判断条件是否为true,如果为true则执行该分支,

注意:排他网关只会选择一个为true的分支执行。如果有两个分支条件都为true,排他网关会选择id值较小的一条分支去执行。

为什么要用排他网关?

不用排他网关也可以实现分支,如:在连线的condition条件上设置分支条件。

在连线设置condition条件的缺点:如果条件都不满足,流程就结束了(是异常结束)。

如果 使用排他网关决定分支的走向,如下:

在这里插入图片描述

如果从网关出去的线所有条件都不满足则系统抛出异常。

1
arduino复制代码org.activiti.engine.ActivitiException: No outgoing sequence flow of the exclusive gateway 'exclusivegateway1' could be selected for continuing the process

4.3.2 并行网关ParallelGateway

并行网关允许将流程分成多条分支,也可以把多条分支汇聚到一起,并行网关的功能是基于进入和外出顺序流的:

fork分支:并行后的所有外出顺序流,为每个顺序流都创建一个并发分支。

join汇聚: 所有到达并行网关,在此等待的进入分支, 直到所有进入顺序流的分支都到达以后, 流程就会通过汇聚网关。

注意,如果同一个并行网关有多个进入和多个外出顺序流, 它就同时具有分支和汇聚功能。 这时,网关会先汇聚所有进入的顺序流,然后再切分成多个并行分支。

与其他网关的主要区别是,并行网关不会解析条件。 即使顺序流中定义了条件,也会被忽略。

在这里插入图片描述

说明:此时会要求技术经理和项目经理都进行审批。而连线上的条件会被忽略。

技术经理和项目经理是两个execution分支,在act_ru_execution表有两条记录分别是技术经理和项目经理,act_ru_execution还有一条记录表示该流程实例。

待技术经理和项目经理任务全部完成,在汇聚点汇聚,通过parallelGateway并行网关。

并行网关在业务应用中常用于会签任务,会签任务即多个参与者共同办理的任务。

4.3.3 包含网关InclusiveGateway

包含网关可以看做是排他网关和并行网关的结合体。

和排他网关一样,你可以在外出顺序流上定义条件,包含网关会解析它们。 但是主要的区别是包含网关可以选择多于一条顺序流,这和并行网关一样。

包含网关的功能是基于进入和外出顺序流的:

分支: 所有外出顺序流的条件都会被解析,结果为true的顺序流会以并行方式继续执行, 会为每个顺序流创建一个分支。

汇聚: 所有并行分支到达包含网关,会进入等待状态, 直到每个包含流程token的进入顺序流的分支都到达。 这是与并行网关的最大不同。换句话说,包含网关只会等待被选中执行了的进入顺序流。 在汇聚之后,流程会穿过包含网关继续执行。

在这里插入图片描述

说明:这里当请假天数超过3天,需要项目经理和人事经理一起审批。而请假天数不超过3填,需要技术经理和人事经理一起审批。

所有符合条件的分支也会在后面进行汇聚。

4.3.4 事件网关EventGateway

事件网关允许根据事件判断流向。网关的每个外出顺序流都要连接到一个中间捕获事件。 当流程到达一个基于事件网关,网关会进入等待状态:会暂停执行。与此同时,会为每个外出顺序流创建相对的事件订阅。

事件网关的外出顺序流和普通顺序流不同,这些顺序流不会真的”执行”, 相反它们让流程引擎去决定执行到事件网关的流程需要订阅哪些事件。 要考虑以下条件:

  1. 事件网关必须有两条或以上外出顺序流;
  2. 事件网关后,只能使用intermediateCatchEvent类型(activiti不支持基于事件网关后连接ReceiveTask)
  3. 连接到事件网关的中间捕获事件必须只有一个入口顺序流。

与事件网关配合使用的intermediateCatchEvent:

在这里插入图片描述

这个事件支持多种事件类型:

Message Event:消息事件

Singal Event: 信号事件

Timer Event: 定时事件

在这里插入图片描述

使用事件网关定义流程:

在这里插入图片描述

4.4 个人任务管理

4.4.1 分配任务负责人

在之前的简单示例中,我们已经可以通过配置Assignee属性来指定任务的负责人。但是我们之前的示例中,是简单的配置为worker、manager、finacer等这样的固定的任务人。但是在实际工作中,往往不会是这样固定的人。可能是对应某个职位或者某个角色的系统用户。这时,这种固定分配的方式就非常不灵活了。这时,就可以使用UEL表达式配合流程变量来灵活指定。例如这样:

在这里插入图片描述

这个assignee0就对应activiti中的一个流程变量。

而如果配置成${user.assignee}表示通过user的getter方法获取属性值

也可以配置成使用具体的方法${user.getUserId()}。

甚至可以结合Spring容易来使用。例如${ldapService.findManagerForEmployee(emp)}
ldapService 是 spring 容器的一个 bean,findManagerForEmployee 是该 bean 的一个方法,emp 是 activiti
流程变量, emp 作为参数传到 ldapService.findManagerForEmployee 方法中。

​ 配置了这个流程后,可以配合流程变量使用。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码 /**
* 设置流程负责人
*/
@Test
public void assigneeUEL(){
// 获取流程引擎
ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
// 获取 RuntimeService
RuntimeService runtimeService = processEngine.getRuntimeService();
// 设置assignee的取值,用户可以在界面上设置流程的执行
Map<String,Object> assigneeMap = new HashMap<>();
assigneeMap.put("assignee0","张三");
assigneeMap.put("assignee1","李经理");
assigneeMap.put("assignee2","王总经理");
assigneeMap.put("assignee3","赵财务");
// 启动流程实例,同时还要设置流程定义的assignee的值
runtimeService.startProcessInstanceByKey("myEvection1",assigneeMap);
// 输出
System.out.println(processEngine.getName());
}

执行完成后,可以在act_ru_variable表中看到刚才map中的数据

在这里插入图片描述

注意事项

由于使用表达式分配,必须保证在任务执行过程中表达式执行成功。否则会抛出activiti异常。

4.5 组任务分配

4.5.1 设置多个候选责任人

​ 之前我们已经可以给任务灵活的设定负责人。但是在日常工作中,还有一类非常常见的需求无法支持。例如某个订单合同,需要找部门经理级别的负责人签字。而公司中有多个部门经理,业务上只需要找其中任意一个人完成审批就可以了。 这种场景下,我们就无法通过设置流程变量的方式来设置负责人。这时,就需要用到Activiti提供的另一个利器-任务候选人Candidate Users。

这时,可以给任务设置多个候选人 candidate-uses,多个候选人之间用逗号隔开。

在这里插入图片描述

在BPMN文件中可以看到

1
xml复制代码<userTask activiti:candidateUsers="lisi,wangwu" activiti:exclusive="true" id="_3" name="经理审批"/>

这样就给这个任务设置了一组候选人。

4.5.2 组任务办理流程

给任务分配了候选人后,后续就需要这些候选人主动认领自己的业务,然后进行处理。

1、查询组任务

指定候选人,查询该候选人当前的待办任务。候选人不能立即办理任务,需要先认领业务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码@Test
public void findGroupTaskList() {
// 流程定义key
String processDefinitionKey = "evection3";
// 任务候选人
String candidateUser = "lisi";
// 获取processEngine
ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
// 创建TaskService
TaskService taskService = processEngine.getTaskService();
//查询组任务
List<Task> list = taskService.createTaskQuery()
.processDefinitionKey(processDefinitionKey)
.taskCandidateUser(candidateUser)//根据候选人查询
.list();
for (Task task : list) {
System.out.println("----------------------------");
System.out.println("流程实例id:" + task.getProcessInstanceId());
System.out.println("任务id:" + task.getId());
System.out.println("任务负责人:" + task.getAssignee());
System.out.println("任务名称:" + task.getName());
}
}
2、拾取(claim)任务

该组任务的所有候选人都能拾取。将候选人的组任务,变成个人任务。原来候选人就变成了该任务的负责人。如果拾取后不想办理该任务,负责人也可以将已经拾取的个人任务归还到组里边,将个人任务变成了组任务。

候选人认领组任务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码@Test
public void claimTask(){
// 获取processEngine
ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
TaskService taskService = processEngine.getTaskService();
//要拾取的任务id
String taskId = "6302";
//任务候选人id
String userId = "lisi";
//拾取任务
//即使该用户不是候选人也能拾取(建议拾取时校验是否有资格)
//校验该用户有没有拾取任务的资格
Task task = taskService.createTaskQuery()
.taskId(taskId)
.taskCandidateUser(userId)//根据候选人查询
.singleResult();
if(task!=null){
//拾取任务
taskService.claim(taskId, userId);
System.out.println("任务拾取成功");
}
}

注:Activiti中,即使该用户不是候选人,也能认领热舞。所以建议要在业务中自行校验是否有资格。

任务被认领后,该任务就有了具体的负责人。其他候选人将查询不到该任务。

3、查询个人任务

与原有的流程相同

4、办理个人任务

与原有的流程相同

5、归还组任务

如果个人不想办理该组任务,可以在认领之后归还组任务,归还后该用户就不再是该任务的负责人了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
java复制代码/*
*归还组任务,由个人任务变为组任务,还可以进行任务交接
*/
@Test
public void setAssigneeToGroupTask() {
// 获取processEngine
ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
// 查询任务使用TaskService
TaskService taskService = processEngine.getTaskService();
// 当前待办任务
String taskId = "6004";
// 任务负责人
String userId = "zhangsan2";
// 校验userId是否是taskId的负责人,如果是负责人才可以归还组任务
Task task = taskService
.createTaskQuery()
.taskId(taskId)
.taskAssignee(userId)
.singleResult();
if (task != null) {
// 如果设置为null,归还组任务,该 任务没有负责人
taskService.setAssignee(taskId, null);
}
}

注:从这个代码中可以看到,实际上是允许直接给任务设定责任人的,即使被委托用户不是候选人,也可以直接指定。

这就是任务交接的流程。

数据库表操作

查询当前任务执行表

1
sql复制代码SELECT * FROM act_ru_task

任务执行表,记录当前执行的任务,由于该任务当前是组任务,所有assignee为空,当拾取任务后该字段就是拾取用户的id

查询任务参与者

1
sql复制代码SELECT * FROM act_ru_identitylink

任务参与者,记录当前参考任务用户或组,当前任务如果设置了候选人,会向该表插入候选人记录,有几个候选就插入几个

与act_ru_identitylink对应的还有一张历史表act_hi_identitylink,向act_ru_identitylink插入记录的同时也会向历史表插入记录。任务完成

五、Activiti与Spring整合

Activiti与Spring整合的基本思想是将Activiti最为核心的ProcessEngine类交由Spring容器进行管理。

核心的pom依赖:

1
2
3
4
5
xml复制代码 <groupId>org.activiti</groupId>
<artifactId>activiti-spring</artifactId>
<version>7.0.0.Beta1</version>
</dependency>
<dependency>

然后在classpath下创建activiti-spring.xml文件

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
xml复制代码<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- 数据源 -->
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/activiti"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
<property name="maxActive" value="3"/>
<property name="maxIdle" value="1"/>
</bean>
<!-- 工作流引擎配置bean -->
<bean id="processEngineConfiguration" class="org.activiti.spring.SpringProcessEngineConfiguration">
<!-- 数据源 -->
<property name="dataSource" ref="dataSource"/>
<!-- 使用spring事务管理器 -->
<property name="transactionManager" ref="transactionManager"/>
<!-- 数据库策略 -->
<property name="databaseSchemaUpdate" value="drop-create"/>
</bean>
<!-- 流程引擎 -->
<bean id="processEngine" class="org.activiti.spring.ProcessEngineFactoryBean">
<property name="processEngineConfiguration" ref="processEngineConfiguration"/>
</bean>
<!-- 资源服务service -->
<bean id="repositoryService" factory-bean="processEngine" factory-method="getRepositoryService"/>
<!-- 流程运行service -->
<bean id="runtimeService" factory-bean="processEngine" factory-method="getRuntimeService"/>
<!-- 任务管理service -->
<bean id="taskService" factory-bean="processEngine" factory-method="getTaskService"/>
<!-- 历史管理service -->
<bean id="historyService" factory-bean="processEngine" factory-method="getHistoryService"/>
<!-- 事务管理器 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- 通知 -->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<!-- 传播行为 -->
<tx:method name="save*" propagation="REQUIRED"/>
<tx:method name="insert*" propagation="REQUIRED"/>
<tx:method name="delete*" propagation="REQUIRED"/>
<tx:method name="update*" propagation="REQUIRED"/>
<tx:method name="find*" propagation="SUPPORTS" read-only="true"/>
<tx:method name="get*" propagation="SUPPORTS" read-only="true"/>
</tx:attributes>
</tx:advice>
</beans>

然后就可以从Spring容器中直接引用对应的service,进行具体的业务操作了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码/**
测试activiti与spring整合是否成功
**/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:activiti-spring.xml")
public class ActivitiTest {
@Autowired
private RepositoryService repositoryService;

@Test
public void test01(){
System.out.println("部署对象:"+repositoryService);
}
}

下面我们一起来分析Activiti与Spring整合加载的过程。

1、加载activiti-spring.xml配置文件

2、加载SpringProcessEngineConfiguration对象,这个对象它需要依赖注入dataSource对象和transactionManager对象。

3、加载ProcessEngineFactoryBean工厂来创建ProcessEngine对象,而ProcessEngineFactoryBean工厂又需要依赖注入processEngineConfiguration对象。

4、processEngine对象来负责创建我们的Service对象,从而简化Activiti的开发过程。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

一口气说出 Redis 16 个常见使用场景

发表于 2021-08-09

1、缓存

String类型

例如:热点数据缓存(例如报表、明星出轨),对象缓存、全页缓存、可以提升热点数据的访问数据。

2、数据共享分布式

String 类型,因为 Redis 是分布式的独立服务,可以在多个应用之间共享

例如:分布式Session

1
2
3
4
xml复制代码<dependency> 
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>

3、分布式锁

String 类型setnx方法,只有不存在时才能添加成功,返回true

1
2
3
4
5
6
7
8
9
10
11
java复制代码public static boolean getLock(String key) {
Long flag = jedis.setnx(key, "1");
if (flag == 1) {
jedis.expire(key, 10);
}
return flag == 1;
}

public static void releaseLock(String key) {
jedis.del(key);
}

4、全局ID

int类型,incrby,利用原子性

incrby userid 1000

分库分表的场景,一次性拿一段

5、计数器

int类型,incr方法

例如:文章的阅读量、微博点赞数、允许一定的延迟,先写入Redis再定时同步到数据库

6、限流

int类型,incr方法

以访问者的ip和其他信息作为key,访问一次增加一次计数,超过次数则返回false

7、位统计

String类型的bitcount(1.6.6的bitmap数据结构介绍)

字符是以8位二进制存储的

1
2
3
4
5
6
7
8
9
10
shell复制代码set k1 a
setbit k1 6 1
setbit k1 7 0
get k1
/* 6 7 代表的a的二进制位的修改
a 对应的ASCII码是97,转换为二进制数据是01100001
b 对应的ASCII码是98,转换为二进制数据是01100010

因为bit非常节省空间(1 MB=8388608 bit),可以用来做大数据量的统计。
*/

例如:在线用户统计,留存用户统计

1
2
3
shell复制代码setbit onlineusers 01 
setbit onlineusers 11
setbit onlineusers 20

支持按位与、按位或等等操作

1
2
3
4
shell复制代码BITOPANDdestkeykey[key...] ,对一个或多个 key 求逻辑并,并将结果保存到 destkey 。       
BITOPORdestkeykey[key...] ,对一个或多个 key 求逻辑或,并将结果保存到 destkey 。
BITOPXORdestkeykey[key...] ,对一个或多个 key 求逻辑异或,并将结果保存到 destkey 。
BITOPNOTdestkeykey ,对给定 key 求逻辑非,并将结果保存到 destkey 。

计算出7天都在线的用户

1
shell复制代码BITOP "AND" "7_days_both_online_users" "day_1_online_users" "day_2_online_users" ...  "day_7_online_users"

8、购物车

String 或hash。所有String可以做的hash都可以做

  • key:用户id;field:商品id;value:商品数量。
  • +1:hincr。-1:hdecr。删除:hdel。全选:hgetall。商品数:hlen。

9、用户消息时间线timeline

list,双向链表,直接作为timeline就好了。插入有序

10、消息队列

List提供了两个阻塞的弹出操作:blpop/brpop,可以设置超时时间

  • blpop:blpop key1 timeout 移除并获取列表的第一个元素,如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。
  • brpop:brpop key1 timeout 移除并获取列表的最后一个元素,如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。

上面的操作。其实就是java的阻塞队列。学习的东西越多。学习成本越低

  • 队列:先进先除:rpush blpop,左头右尾,右边进入队列,左边出队列
  • 栈:先进后出:rpush brpop

11、抽奖

自带一个随机获得值

1
shell复制代码spop myset

12、点赞、签到、打卡

假如上面的微博ID是t1001,用户ID是u3001

用 like:t1001 来维护 t1001 这条微博的所有点赞用户

  • 点赞了这条微博:sadd like:t1001 u3001
  • 取消点赞:srem like:t1001 u3001
  • 是否点赞:sismember like:t1001 u3001
  • 点赞的所有用户:smembers like:t1001
  • 点赞数:scard like:t1001

是不是比数据库简单多了。

13、商品标签

img

老规矩,用 tags:i5001 来维护商品所有的标签。

  • sadd tags:i5001 画面清晰细腻
  • sadd tags:i5001 真彩清晰显示屏
  • sadd tags:i5001 流程至极

14、商品筛选

1
2
3
4
5
6
shell复制代码// 获取差集
sdiff set1 set2
// 获取交集(intersection )
sinter set1 set2
// 获取并集
sunion set1 set2

假如:iPhone11 上市了

1
2
3
4
5
6
7
shell复制代码sadd brand:apple iPhone11

sadd brand:ios iPhone11

sad screensize:6.0-6.24 iPhone11

sad screentype:lcd iPhone 11

赛选商品,苹果的、ios的、屏幕在6.0-6.24之间的,屏幕材质是LCD屏幕

1
shell复制代码sinter brand:apple brand:ios screensize:6.0-6.24 screentype:lcd

15、用户关注、推荐模型

follow 关注 fans 粉丝

相互关注:

  • sadd 1:follow 2
  • sadd 2:fans 1
  • sadd 1:fans 2
  • sadd 2:follow 1

我关注的人也关注了他(取交集):

  • sinter 1:follow 2:fans

可能认识的人:

  • 用户1可能认识的人(差集):sdiff 2:follow 1:follow
  • 用户2可能认识的人:sdiff 1:follow 2:follow

16、排行榜

id 为6001 的新闻点击数加1:

1
复制代码zincrby hotNews:20190926 1 n6001

获取今天点击最多的15条:

1
复制代码zrevrange hotNews:20190926 0 15 withscores

Redis 用的好,加薪少不了

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

字节跳动《实时音视频通讯技术》学习笔记之RTC概述及技术简介

发表于 2021-08-09

这是我参与8月更文挑战的第9天,活动详情查看:8月更文挑战

什么是实时音视频

实时音视频(RTC)即基于IP技术实现的实时交互的音视频通信技术。

RTC 与 直播常用协议的区别

直播协议 播放延迟
FLV 3s-5s
RTMP 3s-5s
HLS 10s+

而这里我们要使用的RTC技术就厉害了~

它是基于IP技术的,它的延迟低于400ms,RTC传输的内容是音视频数据。

实时音视频应用场景

  • 音视频通话
+ 产品功能


1V1,多人音视频通话


可以美颜、使用道具等等。
+ 技术特点


支持设备差异性大


网路接入经常切换

因为这种产品主要是面向用户的,不同用户使用的设备的差别比较大。根据不同设备需要做不同的优化。这就是为什么我们说支持设备差异性大。

而在实际情况中,经常遇到移动网络4G、5G切换WIFI,或者基站之间的切换。这些导致网络环境的变化需要中断重连。

下面介绍两种场景:抖音直播和直播连麦。

  • 抖音直播
+ 产品功能Ⅰ


    - 电商直播
    - 游戏直播
    - 秀场直播
+ 技术特点Ⅰ


    - 主播段推流
    - 观众端CDN拉流
  • 直播连麦
+ 产品功能Ⅱ


    - 多个主播同框互动,观众围观实况
    - K歌、游戏互动、互动交流
+ 技术特点Ⅱ


    - 服务端&客户端合流
    - 合流转推
    - 实时审核

直播连麦将多个主播的视频流合流然后发送给观众。这种合流一般是在服务端做的,但是现在由于客户端的性能不断提高,现在出现了将合流放在客户端的情况,这样节约了成本。

我们都知道传统直播技术的延迟比较高,从观众评论到看到主播反馈一般要5-10秒以上,那么这样在教育直播、电商直播、体育直播等直播就会出现一些问题。

前面我们提到RTC能够实现低延迟的实时传输音视频流,那么RTC可以应用在直播场景吗?

答案是是,因为只要我们将基于TCP的网络传输协议转化为基于UDP的RTC就行了。

那为什么我们不一开始就使用RTC呢?

第一因为成本,CDN的成本是RTC的三分之一,RTC的部署是比较消耗资源的。

第二是因为RTC是需要做很多网络的优化的,比较复杂。

普通直播替换为低延时直播的方案

方案Ⅰ

拉流端(播放端)替换为RTC:收益大。

因为观众端的延时比较大,所以一般是从观众端替换为RTC。

方案Ⅱ

推流端(主播端)替换为RTC:收益中。

因为主播网络环境一般还不错,所以不优先考虑主播端。

RTC应用场景:在线教育

  • 一对一教育
+ 产品功能


    - 1V1 教学
    - 白板、课件
    - 云端录制
    - 监课
+ 技术特点


    - 课件同步
    - 音视频通话类似
    - 可能需要跨国要求和音视频通话一样,需要及时反馈,需要低延迟,跨国一对一可能物理距离较远,导致延迟可能较高。
  • 大班课
+ 产品功能


    - 万人课堂
    - 白板、课件
    - 云端录制
    - 监课
+ 技术特点


    - 1人发布
    - 课件同步

大班课技术难度比1V1教育低,因为一般情况下只是老师一个人推流,不存在过多互动。总的来看,大班课互动性较差,学习体验可能不是很好。

于是,小班课就产生了,它有较强的互动性,但是其难度最大,比1v1教育难度要高。因为每个人网络环境不一样,需要给不同用户下发不同码率的视频。

  • 小班课
+ 产品功能


    - 多人互动
    - 白板、课件
    - 云端录制
    - 监课
+ 技术特点


    - 多人发布与订阅
    - 课件同步

RTC使用场景:视频会议

  • 飞书视频会议
+ 产品功能


    - 百(千)人视频互动
    - 屏幕共享
    - 文档分享
    - PSTN接入
    - 背景虚化,美颜...
+ 技术特点


    - 多人音视频互动
    - 接入设备多样性
    - 音频降噪
    - 弱网优化
    - AI能力

总体来说,视频会议的技术难度较大,对音频降噪的要求比较高,同时存在PSTN接入的情况。

RTC使用场景:游戏

  • 游戏对战
+ 产品功能


    - 小队语音
    - 范围语音
+ 技术特点


    - 低延迟、低耗能、流量小
    - 范围语音

因为游戏比较耗计算机资源和网络资源,又要求低延迟。所以需要达到低延迟、低耗能、流量小。

  • 云游戏
+ 产品功能


    - 游戏运行在服务端
    - 客户端渲染、控制
+ 技术特点


    - 超低延迟
    - 海量控制指令

这样即使设备性能不高也能实现尝试高性能的游戏。适用于大型游戏和游戏试玩。

因为需要良好的游戏体验,就需要超低延迟。而且因为我们RTC可以传输海量的控制指令,所以可以用于云游戏。

实时音视频技术概览

RTC系统架构图

image-20210808095728233

信令是一些控制指令,信令服务器可以用于呼叫、协调。

合流转推等等这些操作是后处理服务器来完成的。

客户端是音视频通话的终端,我们来看看客户端整体技术架构。

image.png

QoS是保证在弱网的情况下仍然能够使用。

事件上报是因为任何的日志都需要上传,可以处理错误和进行性能优化、算法改进。

  • 全平台支持
+ 设备适配
+ 性能适配
  • 连接保持
+ 断网重连
+ 多径传输
  • 数据运营
+ 事件上报
+ 日志收集

低性能的设备使用低性能的算法。

同时支持WIFI、4G就需要实现多径传输。

image.png

采集到音视频等数据需要进行编码压缩然后通过网络传输,然后解码播放。

信令服务器

信令:为使网络中各种设备协调运作,在设备之间传递的控制信息。

信令服务器:就是用来传输、中专信令的服务器。

  • 常见问题
+ 全球化部署
+ 信令到达率
+ 连接保持
  • 实现方案
+ WebSocket
+ 自定义协议

媒体服务器

媒体服务器:在终端用户之间中转音视频流,进而让用户之间可以进行音视频通信。通常部署在边缘,距离用户较近的地方。

image.png

Simulcast&SVC是根据不同用户的网络状况提供不同码率、帧率的视频。

BWE&拥塞控制是用来估计用户的可用带宽,来判断给用户发送多大码率的码流。

下面来看看几种媒体服务器的典型架构:

image.png

后处理

  • 音视频录制
  • 合流转推
  • 截图、切片
  • 审核

还有什么?

  • 数据运营
  • 质量评估
  • QoS
  • 自动化测试
  • 应用场景探索

需要数据才能优化,视频是否清晰,音频是否悦耳这就需要质量评估。

自动化测试和质量评估也是比较重要的。

去探索新的应用场景也是非常重要的。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Tomcat8源码解析(二)

发表于 2021-08-09
2.Tomcat启动阶段
  • daemon.start(),tomcat的启动阶段分析
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
java复制代码# 1.Bootstrap的start()方法
public void start()
throws Exception {
if( catalinaDaemon==null ) init();
//实际上是执行了catalina的start()方法
Method method = catalinaDaemon.getClass().getMethod("start", (Class [] )null);
method.invoke(catalinaDaemon, (Object [])null);
}

# 1.Catalina的start()方法
public void start() {
if (getServer() == null) {
load();
}
if (getServer() == null) {
log.fatal("Cannot start server. Server instance is not configured.");
return;
}
long t1 = System.nanoTime();
// Start the new server
try {
//启动Server(重点)
getServer().start();
} catch (LifecycleException e) {
log.fatal(sm.getString("catalina.serverStartFail"), e);
try {
getServer().destroy();
} catch (LifecycleException e1) {
log.debug("destroy() failed for failed Server ", e1);
}
return;
}

long t2 = System.nanoTime();
if(log.isInfoEnabled()) {
log.info("Server startup in " + ((t2 - t1) / 1000000) + " ms");
}

// Register shutdown hook 钩子
if (useShutdownHook) {
if (shutdownHook == null) {
shutdownHook = new CatalinaShutdownHook();
}
Runtime.getRuntime().addShutdownHook(shutdownHook);
// If JULI is being used, disable JULI's shutdown hook since
// shutdown hooks run in parallel and log messages may be lost
// if JULI's hook completes before the CatalinaShutdownHook()
LogManager logManager = LogManager.getLogManager();
if (logManager instanceof ClassLoaderLogManager) {
((ClassLoaderLogManager) logManager).setUseShutdownHook(
false);
}
}
if (await) {
await();
stop();
}
}
  • getServer().start(),方法启动Server,源码分析
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
java复制代码# 1.LifecycleBase的start()方法
@Override
public final synchronized void start() throws LifecycleException {
if (LifecycleState.STARTING_PREP.equals(state) || LifecycleState.STARTING.equals(state) ||
LifecycleState.STARTED.equals(state)) {

if (log.isDebugEnabled()) {
Exception e = new LifecycleException();
log.debug(sm.getString("lifecycleBase.alreadyStarted", toString()), e);
} else if (log.isInfoEnabled()) {
log.info(sm.getString("lifecycleBase.alreadyStarted", toString()));
}

return;
}

if (state.equals(LifecycleState.NEW)) {
init();
} else if (state.equals(LifecycleState.FAILED)) {
stop();
} else if (!state.equals(LifecycleState.INITIALIZED) &&
!state.equals(LifecycleState.STOPPED)) {
invalidTransition(Lifecycle.BEFORE_START_EVENT);
}

try {
setStateInternal(LifecycleState.STARTING_PREP, null, false);

//启动子类的StandardServer(重点)
startInternal();

if (state.equals(LifecycleState.FAILED)) {
// This is a 'controlled' failure. The component put itself into the
// FAILED state so call stop() to complete the clean-up.
stop();
} else if (!state.equals(LifecycleState.STARTING)) {
// Shouldn't be necessary but acts as a check that sub-classes are
// doing what they are supposed to.
invalidTransition(Lifecycle.AFTER_START_EVENT);
} else {
setStateInternal(LifecycleState.STARTED, null, false);
}
} catch (Throwable t) {
// This is an 'uncontrolled' failure so put the component into the
// FAILED state and throw an exception.
ExceptionUtils.handleThrowable(t);
setStateInternal(LifecycleState.FAILED, null, false);
throw new LifecycleException(sm.getString("lifecycleBase.startFail", toString()), t);
}
}

# 2.standardServer的startInternal()方法
@Override
protected void startInternal() throws LifecycleException {

fireLifecycleEvent(CONFIGURE_START_EVENT, null);
setState(LifecycleState.STARTING);

globalNamingResources.start();

// Start our defined Services
synchronized (servicesLock) {
for (int i = 0; i < services.length; i++) {
//启动services(重点)
services[i].start();
}
}
}

Server的初始化start()方法,会先调用父类LifecycleBase的start()方法,然后再调用子类Server的startInternal()方法
这个调用的模式:父类start—>子类startInternal,在后面的services,connector,engine初始化是一样的模式。

  • services[i].start(),启动services,源码分析
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
java复制代码# 1.standardService的startInternal()方法
@Override
protected void startInternal() throws LifecycleException {

if(log.isInfoEnabled())
log.info(sm.getString("standardService.start.name", this.name));
setState(LifecycleState.STARTING);

//启动engine(重点)
// Start our defined Container first
if (engine != null) {
synchronized (engine) {
engine.start();
}
}

synchronized (executors) {
for (Executor executor: executors) {
executor.start();
}
}

//启动mapperListener
mapperListener.start();

// Start our defined Connectors second
synchronized (connectorsLock) {
for (Connector connector: connectors) {
try {
// If it has already failed, don't try and start it
if (connector.getState() != LifecycleState.FAILED) {
//启动connector(重点)
connector.start();
}
} catch (Exception e) {
log.error(sm.getString(
"standardService.connector.startFailed",
connector), e);
}
}
}
}

从services的启动分析,可以得到services的启动,会启动engine和connector。(executor、mapperListener)

  • connector.start(),启动Connector,源码分析
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
java复制代码# 1.Connector的startInternal()方法
@Override
protected void startInternal() throws LifecycleException {

// Validate settings before starting
if (getPort() < 0) {
throw new LifecycleException(sm.getString(
"coyoteConnector.invalidPort", Integer.valueOf(getPort())));
}
setState(LifecycleState.STARTING);
try {
//启动protocolHandler(重点)
protocolHandler.start();
} catch (Exception e) {
throw new LifecycleException(
sm.getString("coyoteConnector.protocolHandlerStartFailed"), e);
}
}

# 2.AbstractProtocol下protocolHandler的启动start()方法
@Override
public void start() throws Exception {
if (getLog().isInfoEnabled()) {
getLog().info(sm.getString("abstractProtocolHandler.start", getName()));
}

//启动endpoint
endpoint.start();

// Start async timeout thread
asyncTimeout = new AsyncTimeout();
Thread timeoutThread = new Thread(asyncTimeout, getNameInternal() + "-AsyncTimeout");
int priority = endpoint.getThreadPriority();
if (priority < Thread.MIN_PRIORITY || priority > Thread.MAX_PRIORITY) {
priority = Thread.NORM_PRIORITY;
}
timeoutThread.setPriority(priority);
timeoutThread.setDaemon(true);
timeoutThread.start();
}

# 3.AbstractEndpoint下endpoint的启动start()方法
public final void start() throws Exception {
if (bindState == BindState.UNBOUND) {
bind();
bindState = BindState.BOUND_ON_START;
}
startInternal();
}

从connector的启动分析,可以得到connector的启动,会启动protocolHandler和endpoint

  • engine.start(),启动engine,源码分析
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
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
java复制代码# 1.StandardEngine的startInternal()方法
@Override
protected synchronized void startInternal() throws LifecycleException {

// Log our server identification information
if(log.isInfoEnabled())
log.info( "Starting Servlet Engine: " + ServerInfo.getServerInfo());

// Standard container startup
super.startInternal();
}

# 2.ContainerBase的startInternal()方法
@Override
protected synchronized void startInternal() throws LifecycleException {

// Start our subordinate components, if any
logger = null;
//日志处理
getLogger();
//有集群启动集群
Cluster cluster = getClusterInternal();
if (cluster instanceof Lifecycle) {
((Lifecycle) cluster).start();
}
//域处理
Realm realm = getRealmInternal();
if (realm instanceof Lifecycle) {
((Lifecycle) realm).start();
}

//启动所有子容器 StandardHost, StandardContext, StandardWrapper
Container children[] = findChildren();
List<Future<Void>> results = new ArrayList<>();
for (int i = 0; i < children.length; i++) {
results.add(startStopExecutor.submit(new StartChild(children[i])));
}
MultiThrowable multiThrowable = null;

//开启线程启动子容器
for (Future<Void> result : results) {
try {
//阻塞,让子容器启动完成再执行下面的代码
result.get();
} catch (Throwable e) {
log.error(sm.getString("containerBase.threadedStartFailed"), e);
if (multiThrowable == null) {
multiThrowable = new MultiThrowable();
}
multiThrowable.add(e);
}
}
if (multiThrowable != null) {
throw new LifecycleException(sm.getString("containerBase.threadedStartFailed"),
multiThrowable.getThrowable());
}
//启用Pipeline管道
// Start the Valves in our pipeline (including the basic), if any
if (pipeline instanceof Lifecycle) {
((Lifecycle) pipeline).start();
}
//设置生命周期的状态starting,激发监听器listener:HostConfig,通过这个去启动Host(重要)
setState(LifecycleState.STARTING);
//开启线程
threadStart();
}

# 3.StandardHost的startInternal()方法
@Override
protected synchronized void startInternal() throws LifecycleException {
//设置错误报告
// Set error report valve
String errorValve = getErrorReportValveClass();
if ((errorValve != null) && (!errorValve.equals(""))) {
try {
boolean found = false;
Valve[] valves = getPipeline().getValves();
for (Valve valve : valves) {
if (errorValve.equals(valve.getClass().getName())) {
found = true;
break;
}
}
if(!found) {
Valve valve =
(Valve) Class.forName(errorValve).getConstructor().newInstance();
getPipeline().addValve(valve);
}
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
log.error(sm.getString(
"standardHost.invalidErrorReportValveClass",
errorValve), t);
}
}
//再次调用父类ContainerBase
super.startInternal();
}

# 4.LifecycleBase.setState(LifecycleState.STARTING); 改变生命周期的状态,激发监听器listener:HostConfig。
protected synchronized void setState(LifecycleState state) throws LifecycleException {
setStateInternal(state, null, true);
}

private synchronized void setStateInternal(LifecycleState state,
Object data, boolean check) throws LifecycleException {
this.state = state;
String lifecycleEvent = state.getLifecycleEvent();
if (lifecycleEvent != null) {
fireLifecycleEvent(lifecycleEvent, data);
}
}

protected void fireLifecycleEvent(String type, Object data) {
LifecycleEvent event = new LifecycleEvent(this, type, data);
for (LifecycleListener listener : lifecycleListeners) {
//这个listener就是hostConfig(重点)
listener.lifecycleEvent(event);
}
}

# 5.HostConfig的start()
public void lifecycleEvent(LifecycleEvent event) {
// Process the event that has occurred
if (event.getType().equals(Lifecycle.PERIODIC_EVENT)) {
check();
} else if (event.getType().equals(Lifecycle.BEFORE_START_EVENT)) {
beforeStart();
} else if (event.getType().equals(Lifecycle.START_EVENT)) {
//执行hostConfig的start()方法
start();
} else if (event.getType().equals(Lifecycle.STOP_EVENT)) {
stop();
}
}

public void start() {
...
//部署webapps
if (host.getDeployOnStartup())
deployApps();

}

# 6.部署webapps
protected void deployApps() {
File appBase = host.getAppBaseFile();
File configBase = host.getConfigBaseFile();
String[] filteredAppPaths = filterAppPaths(appBase.list());

//部署xml文件配置的server.xml <host><context>...
// Deploy XML descriptors from configBase
deployDescriptors(configBase, configBase.list());

//部署war包
// Deploy WARs
deployWARs(appBase, filteredAppPaths);

//部署文件夹
// Deploy expanded folders
deployDirectories(appBase, filteredAppPaths);
}

# 7.部署文件夹deployDirectories()
protected void deployDirectories(File appBase, String[] files) {
//发布文件夹
if (files == null)
return;

//使用future和线程池的技术
ExecutorService es = host.getStartStopExecutor();
List<Future<?>> results = new ArrayList<>();

for (int i = 0; i < files.length; i++) {

if (files[i].equalsIgnoreCase("META-INF"))
continue;
if (files[i].equalsIgnoreCase("WEB-INF"))
continue;
File dir = new File(appBase, files[i]);
if (dir.isDirectory()) {
ContextName cn = new ContextName(files[i], false);

if (isServiced(cn.getName()) || deploymentExists(cn.getName()))
continue;

//submit(Runnable),接收一个Runnable,会返回一个Future
//new DeployDirectory(重点)
results.add(es.submit(new DeployDirectory(this, cn, dir)));
}
}

//如果任务结束执行则返回null(会等待线程执行完)
for (Future<?> result : results) {
try {
result.get();
} catch (Exception e) {
log.error(sm.getString(
"hostConfig.deployDir.threaded.error"), e);
}
}
}

//实现runnable接口
private static class DeployDirectory implements Runnable {

private HostConfig config;
private ContextName cn;
private File dir;

public DeployDirectory(HostConfig config, ContextName cn, File dir) {
this.config = config;
this.cn = cn;
this.dir = dir;
}

@Override
public void run() {
//部署文件夹
config.deployDirectory(cn, dir);
}
}

# 8.部署文件夹deployDirectory()
protected void deployDirectory(ContextName cn, File dir) {

long startTime = 0;
// Deploy the application in this directory
if( log.isInfoEnabled() ) {
startTime = System.currentTimeMillis();
log.info(sm.getString("hostConfig.deployDir",
dir.getAbsolutePath()));
}

//拿到context
Context context = null;
File xml = new File(dir, Constants.ApplicationContextXml);
File xmlCopy = new File(host.getConfigBaseFile(), cn.getBaseName() + ".xml");


DeployedApplication deployedApp;
boolean copyThisXml = isCopyXML();
boolean deployThisXML = isDeployThisXML(dir, cn);

try {
if (deployThisXML && xml.exists()) {
synchronized (digesterLock) {
try {
//解析context节点
context = (Context) digester.parse(xml);
}
}
} else if (!deployThisXML && xml.exists()) {
context = new FailedContext();
} else {
context = (Context) Class.forName(contextClass).getConstructor().newInstance();
}

//实例化 ContextConfig,作为 LifecycleListener 添加到 Context 容器中
//这和 StandardHost 的套路一样,都是使用 XXXConfig
Class<?> clazz = Class.forName(host.getConfigClass());
LifecycleListener listener = (LifecycleListener) clazz.getConstructor().newInstance();
context.addLifecycleListener(listener);

context.setName(cn.getName());
context.setPath(cn.getPath());
context.setWebappVersion(cn.getVersion());
context.setDocBase(cn.getBaseName());

//把context添加到子节点,然后启动context(重点)
host.addChild(context);
}
}

从standardEngine的启动过程,可以看出,主要是启动standardHost。
然后通过监听器技术,启动HostConfig,进而解析webapps,启动context。

HostConfig的deployDirectory,主要做了几个工作:
1.使用 digester,或者反射实例化 StandardContext
2.实例化ContextConfig,并且为Context 容器注册事件监听器,和 StandardHost 的套路一样,借助 XXXConfig 完成容器的启动、停止工作
3. 将当前 Context 实例作为子容器添加到Host 容器中,添加子容器的逻辑在 ContainerBase 中已经实现了,如果当前 Container 的状态是 STARTING_PREP 并且 startChildren 为 true,则还会启动子容器

  • StandardContext的startInternal()方法分析
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
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
java复制代码# 1.StandardContext的startInternal()方法
@Override
protected synchronized void startInternal() throws LifecycleException {
//StandardContext启动

if(log.isDebugEnabled())
log.debug("Starting " + getBaseName());

//发布这个状态,广播出去,让其他监听
// Send j2ee.state.starting notification
if (this.getObjectName() != null) {
Notification notification = new Notification("j2ee.state.starting",
this.getObjectName(), sequenceNumber.getAndIncrement());
broadcaster.sendNotification(notification);
}

setConfigured(false);
boolean ok = true;

//启动命名空间资源
// Currently this is effectively a NO-OP but needs to be called to
// ensure the NamingResources follows the correct lifecycle
if (namingResources != null) {
namingResources.start();
}

//创建工作目录work
// Post work directory
postWorkDirectory();

//加载资源
// Add missing components as necessary
if (getResources() == null) { // (1) Required by Loader
if (log.isDebugEnabled())
log.debug("Configuring default Resources");

try {
setResources(new StandardRoot(this));
} catch (IllegalArgumentException e) {
log.error(sm.getString("standardContext.resourcesInit"), e);
ok = false;
}
}
if (ok) {
resourcesStart();
}

//Webapp加载器
if (getLoader() == null) {
WebappLoader webappLoader = new WebappLoader(getParentClassLoader());
webappLoader.setDelegate(getDelegate());
setLoader(webappLoader);
}

//初始化一个cookie
// An explicit cookie processor hasn't been specified; use the default
if (cookieProcessor == null) {
cookieProcessor = new Rfc6265CookieProcessor();
}

//字符集的映射
// Initialize character set mapper
getCharsetMapper();

//依赖关系处理
// Validate required extensions
boolean dependencyCheck = true;
try {
dependencyCheck = ExtensionValidator.validateApplication
(getResources(), this);
} catch (IOException ioe) {
log.error(sm.getString("standardContext.extensionValidationError"), ioe);
dependencyCheck = false;
}

if (!dependencyCheck) {
// do not make application available if dependency check fails
ok = false;
}

//用户命名属性,获取环境变量
// Reading the "catalina.useNaming" environment variable
String useNamingProperty = System.getProperty("catalina.useNaming");
if ((useNamingProperty != null)
&& (useNamingProperty.equals("false"))) {
useNaming = false;
}

if (ok && isUseNaming()) {
if (getNamingContextListener() == null) {
NamingContextListener ncl = new NamingContextListener();
ncl.setName(getNamingContextName());
ncl.setExceptionOnFailedWrite(getJndiExceptionOnFailedWrite());
addLifecycleListener(ncl);
setNamingContextListener(ncl);
}
}

// Standard container startup
if (log.isDebugEnabled())
log.debug("Processing standard container startup");


// Binding thread
ClassLoader oldCCL = bindThread();

//发出一个生命周期事件,触发监听器:ContextConfig(重点)
fireLifecycleEvent(Lifecycle.CONFIGURE_START_EVENT, null);

//启动wrapper子节点(重点)
// Start our child containers, if not already started
for (Container child : findChildren()) {
if (!child.getState().isAvailable()) {
child.start();
}
}
}

# 2.ContextConfig的configureStart()方法
protected synchronized void configureStart() {
//解析web.xml(重点)
//解析servlet,filter,listener
webConfig();
}

protected void webConfig() {
//创建web.xml解析器
WebXmlParser webXmlParser = new WebXmlParser(context.getXmlNamespaceAware(),
context.getXmlValidation(), context.getXmlBlockExternal());

Set<WebXml> defaults = new HashSet<>();
defaults.add(getDefaultWebXmlFragment(webXmlParser));

WebXml webXml = createWebXml();

//解析web.xml
InputSource contextWebXml = getContextWebXmlSource();
if (!webXmlParser.parseWebXml(contextWebXml, webXml, false)) {
ok = false;
}

ServletContext sContext = context.getServletContext();

// Ordering is important here

// Step 1. Identify all the JARs packaged with the application and those
// provided by the container. If any of the application JARs have a
// web-fragment.xml it will be parsed at this point. web-fragment.xml
// files are ignored for container provided JARs.
Map<String,WebXml> fragments = processJarsForWebFragments(webXml, webXmlParser);

// Step 2. Order the fragments.
Set<WebXml> orderedFragments = null;
orderedFragments =
WebXml.orderWebFragments(webXml, fragments, sContext);

// Step 3. Look for ServletContainerInitializer implementations
if (ok) {
processServletContainerInitializers();
}

if (!webXml.isMetadataComplete() || typeInitializerMap.size() > 0) {
// Steps 4 & 5.
processClasses(webXml, orderedFragments);
}

if (!webXml.isMetadataComplete()) {
// Step 6. Merge web-fragment.xml files into the main web.xml
// file.
if (ok) {
ok = webXml.merge(orderedFragments);
}

// Step 7. Apply global defaults
// Have to merge defaults before JSP conversion since defaults
// provide JSP servlet definition.
webXml.merge(defaults);

// Step 8. Convert explicitly mentioned jsps to servlets
if (ok) {
convertJsps(webXml);
}

// Step 9. Apply merged web.xml to Context
if (ok) {
//配置context
configureContext(webXml);
}
} else {
webXml.merge(defaults);
convertJsps(webXml);
configureContext(webXml);
}

if (context.getLogEffectiveWebXml()) {
log.info("web.xml:\n" + webXml.toXml());
}

// Always need to look for static resources
// Step 10. Look for static resources packaged in JARs
if (ok) {
// Spec does not define an order.
// Use ordered JARs followed by remaining JARs
Set<WebXml> resourceJars = new LinkedHashSet<>();
for (WebXml fragment : orderedFragments) {
resourceJars.add(fragment);
}
for (WebXml fragment : fragments.values()) {
if (!resourceJars.contains(fragment)) {
resourceJars.add(fragment);
}
}
processResourceJARs(resourceJars);
// See also StandardContext.resourcesStart() for
// WEB-INF/classes/META-INF/resources configuration
}

// Step 11. Apply the ServletContainerInitializer config to the
// context
if (ok) {
for (Map.Entry<ServletContainerInitializer,
Set<Class<?>>> entry :
initializerClassMap.entrySet()) {
if (entry.getValue().isEmpty()) {
context.addServletContainerInitializer(
entry.getKey(), null);
} else {
context.addServletContainerInitializer(
entry.getKey(), entry.getValue());
}
}
}

// 指定 ServletContext 的相关参数
mergeParameters();

// 调用 ServletContainerInitializer#onStartup()
for (Map.Entry<ServletContainerInitializer, Set<Class<?>>> entry :
initializers.entrySet()) {
try {
entry.getKey().onStartup(entry.getValue(),getServletContext());
} catch (ServletException e) {
log.error(sm.getString("standardContext.sciFail"), e);
ok = false;
break;
}
}

//初始化 Filter
if (ok) {
if (!filterStart()) {
log.error(sm.getString("standardContext.filterFail"));
ok = false;
}
}

//处理 Wrapper 容器(如果Servlet的loadOnStartup >= 0,便会在这一阶段完成 Servlet 的加载)
if (ok) {
if (!loadOnStartup(findChildren())){
log.error(sm.getString("standardContext.servletFail"));
ok = false;
}
}
}

StandardContext 和其他 Container 一样,也是重写了 startInternal 方法。由于涉及到 webapp 的启动流程,需要很多准备工作,比如使用 WebResourceRoot 加载资源文件、利用 Loader 加载 class、使用 JarScanner 扫描 jar 包,等等。
因此StandardContext 的启动逻辑比较复杂,这里描述下几个重要的步骤:

  1. 创建工作目录,比如$CATALINA_HOME\work\Catalina\localhost\examples;实例化 ContextServlet,应用程序拿到的是 ApplicationContext的外观模式
  2. 实例化 WebResourceRoot,默认实现类是 StandardRoot,用于读取 webapp 的文件资源
  3. 实例化 Loader 对象,Loader 是 tomcat 对于 ClassLoader 的封装,用于支持在运行期间热加载 class
  4. 发出 CONFIGURE_START_EVENT 事件,ContextConfig 会处理该事件,主要目的是从 webapp 中读取 servlet 相关的 Listener、Servlet、Filter 等
  5. 实例化 Sesssion 管理器,默认使用 StandardManager
  6. 调用 listenerStart,实例化 servlet 相关的各种 Listener,并且调用
    ServletContextListener
  7. 处理 Filter
  8. 加载 Servlet
  • 触发 CONFIGURE_START_EVENT 事件,触发ContextConfig监听器

ContextConfig 它是一个 LifycycleListener,它在 Context 启动过程中是承担了一个非常重要的角色。StandardContext 会发出 CONFIGURE_START_EVENT 事件,而 ContextConfig 会处理该事件,主要目的是通过 web.xml 或者 Servlet3.0 的注解配置,读取 Servlet 相关的配置信息,比如 Filter、Servlet、Listener 等,其核心逻辑在 ContextConfig#webConfig() 方法中实现。
ContextConfig执行的重要步骤:

  1. 是通过 WebXmlParser 对 web.xml 进行解析,如果存在 web.xml 文件,则会把文件中定义的 Servlet、Filter、Listener 注册到 WebXml 实例中
  2. 如果没有 web.xml 文件,tomcat 会先扫描 WEB-INF/classes 目录下面的 class 文件,然后扫描 WEB-INF/lib 目录下面的 jar 包,解析字节码读取 servlet 相关的注解配置类,这里不得不吐槽下 serlvet3.0 注解,对 servlet 注解的处理相当重量级。tomcat 不会预先把该 class 加载到 jvm 中,而是通过解析字节码文件,获取对应类的一些信息,比如注解、实现的接口等
  • Step 9:往 Context 中添加子容器 Wrapper
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码# 1.context添加wrapper子节点
private void configureContext(WebXml webxml) {
// 设置 Filter 定义
for (FilterDef filter : webxml.getFilters().values()) {
if (filter.getAsyncSupported() == null) {
filter.setAsyncSupported("false");
}
context.addFilterDef(filter);
}
// 设置 FilterMapping,即 Filter 的 URL 映射
for (FilterMap filterMap : webxml.getFilterMappings()) {
context.addFilterMap(filterMap);
}
// 往 Context 中添加子容器 Wrapper,即 Servlet
for (ServletDef servlet : webxml.getServlets().values()) {
Wrapper wrapper = context.createWrapper();
// 省略若干代码。。。
wrapper.setOverridable(servlet.isOverridable());
context.addChild(wrapper);
}
// ......
}
  • 启动 StandardWrapper容器
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
java复制代码# 1.StandardWrapper的startInternal()方法
@Override
protected synchronized void startInternal() throws LifecycleException {
// 发出 j2ee.state.starting 事件通知
if (this.getObjectName() != null) {
Notification notification = new Notification("j2ee.state.starting",
this.getObjectName(),
sequenceNumber++);
broadcaster.sendNotification(notification);
}
// ConainerBase 的启动逻辑
super.startInternal();
setAvailable(0L);

// 发出 j2ee.state.running 事件通知
if (this.getObjectName() != null) {
Notification notification =
new Notification("j2ee.state.running", this.getObjectName(), sequenceNumber++);
broadcaster.sendNotification(notification);
}
}

# 2.StandardWrapper的load()方法
@Override
public synchronized void load() throws ServletException {
// 实例化 Servlet,并且调用 init 方法完成初始化
instance = loadServlet();
if (!instanceInitialized) {
initServlet(instance);
}

if (isJspServlet) {
// 处理 jsp Servlet
StringBuilder oname = new StringBuilder(getDomain());
oname.append(":type=JspMonitor");
oname.append(getWebModuleKeyProperties());
oname.append(",name=");
oname.append(getName());
oname.append(getJ2EEKeyProperties());

try {
jspMonitorON = new ObjectName(oname.toString());
Registry.getRegistry(null, null)
.registerComponent(instance, jspMonitorON, null);
} catch( Exception ex ) {
log.info("Error registering JSP monitoring with jmx " +
instance);
}
}
}

StandardWrapper 没有子容器,启动逻辑相对比较简单清晰,它重写了 startInternal 方法,主要是完成了 jmx 的事件通知,先后向 jmx 发出 starting、running 事件

由前面对 Context 容器的分析可知,Context 完成 Filter 初始化之后,如果 loadOnStartup >= 0 便会调用 load 方法加载 Wrapper 容器。StandardWrapper 使用 InstanceManager 实例化 Servlet,并且调用 Servlet 的 init 方法进行初始化,传入的 ServletConfig 是 StandardWrapperFacade 对象。

总结:
tomcat 实现了 javax.servlet.ServletContext 接口,在 Context 启动的时候会实例化该对象。由 Context 容器通过 web.xml 或者 扫描 class 字节码读取 servlet3.0 的注解配置,从而加载 webapp 定义的 Listener、Servlet、Filter 等 servlet 组件,但是并不会立即实例化对象。全部加载完毕之后,依次对 Listener、Filter、Servlet 进行实例化、并且调用其初始化方法,比如 ServletContextListener#contextInitialized()、Flter#init() 等

到此tomcat的启动阶段就已经完成了。使用的是责任链模式,一步一步的启动。
组件启动的顺序:
Server–>Service–>Engine–>Host–>Context–>Wrapper

  • tomcat初始化和启动流程图:
  • 在这里插入图片描述
3.Tomcat的web请求处理阶段
  • web请求的总体流程图
  • 在这里插入图片描述

由前面的tomcat总体架构可以知道,tomcat是由connector来接收用户的请求,然后再交由container处理。
通过跟踪connector的启动过程,先后启动protocolHandler,然后启动了endpoint,然后启动了Acceptor(用来接收请求)

  • 通过查看NioEndpoint的主要结构,可以看到该类有三个重要的内部类:Acceptor,Poller,SocketProcessor。
  • 在这里插入图片描述
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
java复制代码# 1.NioEndpoint的startInternal()方法
@Override
public void startInternal() throws Exception {

if (!running) {
running = true;
paused = false;

processorCache = new SynchronizedStack<>(SynchronizedStack.DEFAULT_SIZE,
socketProperties.getProcessorCache());
eventCache = new SynchronizedStack<>(SynchronizedStack.DEFAULT_SIZE,
socketProperties.getEventCache());
nioChannels = new SynchronizedStack<>(SynchronizedStack.DEFAULT_SIZE,
socketProperties.getBufferPool());

//创建工作者线程池
// Create worker collection
if ( getExecutor() == null ) {
createExecutor();
}

initializeConnectionLatch();

//启动poller线程,用来轮询检查新的请求
// Start poller threads
pollers = new Poller[getPollerThreadCount()];
for (int i=0; i<pollers.length; i++) {
pollers[i] = new Poller();
Thread pollerThread = new Thread(pollers[i], getName() + "-ClientPoller-"+i);
pollerThread.setPriority(threadPriority);
pollerThread.setDaemon(true);
pollerThread.start();
}

//启动Acceptor线程,用来接收用户请求
startAcceptorThreads();
}
}

# 2.启动Acceptor线程
protected final void startAcceptorThreads() {
//获取Acceptor线程数(默认是1)
int count = getAcceptorThreadCount();
//创建Acceptor数组
acceptors = new Acceptor[count];

for (int i = 0; i < count; i++) {
//创建Acceptor对象,Acceptor继承Runnable
acceptors[i] = createAcceptor();
String threadName = getName() + "-Acceptor-" + i;
acceptors[i].setThreadName(threadName);

//创建Thread对象
Thread t = new Thread(acceptors[i], threadName);
t.setPriority(getAcceptorThreadPriority());
t.setDaemon(getDaemon());

//启动线程
t.start();
}
}

由上面的代码可以看出NioEndpoint在执行startInternal()方法时候,会启动Acceptor线程。Acceptor继承了Runnable,然后使用Thread启动了Acceptor线程。

  • Acceptor run()方法分析
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
java复制代码# 1.Acceptor 用来接收请求
protected class Acceptor extends AbstractEndpoint.Acceptor {
@Override
public void run() {
int errorDelay = 0;
System.out.println("Acceptor 接收者开始执行");
// Loop until we receive a shutdown command
while (running) {
// Loop if endpoint is paused
while (paused && running) {
state = AcceptorState.PAUSED;
try {
Thread.sleep(50);
} catch (InterruptedException e) {
// Ignore
}
}
if (!running) {
break;
}
state = AcceptorState.RUNNING;
try {
//if we have reached max connections, wait
countUpOrAwaitConnection();

SocketChannel socket = null;
try {
//接收请求,拿到socket
socket = serverSock.accept();
} catch (IOException ioe) {
// We didn't get a socket
countDownConnection();
if (running) {
// Introduce delay if necessary
errorDelay = handleExceptionWithDelay(errorDelay);
// re-throw
throw ioe;
} else {
break;
}
}
// Successful accept, reset the error delay
errorDelay = 0;

// Configure the socket
if (running && !paused) {
//设置socket的一些属性
if (!setSocketOptions(socket)) {
closeSocket(socket);
}
} else {
closeSocket(socket);
}
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
log.error(sm.getString("endpoint.accept.fail"), t);
}
}
state = AcceptorState.ENDED;
}
}

# 2.设置socket的一些属性
protected boolean setSocketOptions(SocketChannel socket) {
// Process the connection
try {
//disable blocking, APR style, we are gonna be polling it
socket.configureBlocking(false);
Socket sock = socket.socket();
socketProperties.setProperties(sock);

//SocketChannel转化成nioChannel
NioChannel channel = nioChannels.pop();
if (channel == null) {
SocketBufferHandler bufhandler = new SocketBufferHandler(
socketProperties.getAppReadBufSize(),
socketProperties.getAppWriteBufSize(),
socketProperties.getDirectBuffer());
if (isSSLEnabled()) { //SSL, https
channel = new SecureNioChannel(socket, bufhandler, selectorPool, this);
} else {
//http1.1
channel = new NioChannel(socket, bufhandler);
}
} else {
channel.setIOChannel(socket);
channel.reset();
}
//获取poller对象,注册channel(重要)
getPoller0().register(channel);
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
try {
log.error("",t);
} catch (Throwable tt) {
ExceptionUtils.handleThrowable(tt);
}
// Tell to close the socket
return false;
}
return true;
}

# 3.poller对象,注册channel。Poller.register()方法
public void register(final NioChannel socket) {
socket.setPoller(this);
NioSocketWrapper ka = new NioSocketWrapper(socket, NioEndpoint.this);
socket.setSocketWrapper(ka);
ka.setPoller(this);
ka.setReadTimeout(getSocketProperties().getSoTimeout());
ka.setWriteTimeout(getSocketProperties().getSoTimeout());
ka.setKeepAliveLeft(NioEndpoint.this.getMaxKeepAliveRequests());
ka.setSecure(isSSLEnabled());
ka.setReadTimeout(getConnectionTimeout());
ka.setWriteTimeout(getConnectionTimeout());

PollerEvent r = eventCache.pop();
//生成poller, socket加入到even queue
ka.interestOps(SelectionKey.OP_READ);//this is what OP_REGISTER turns into.
if ( r==null) r = new PollerEvent(socket,ka,OP_REGISTER);
else r.reset(socket,ka,OP_REGISTER);
addEvent(r);
}

通过查看Acceptor的run方法,可以看到拿到socket对象。(说明tomcat的底层是通过socket通信的)
然后就通过生成poller。Poller线程主要用于以较少的资源轮询已连接套接字以保持连接,当数据可用时转给工作线程。

  • Poller 轮询,检验是否有新的请求
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
java复制代码# 1.Poller的run()方法
@Override
public void run() {
// Loop until destroy() is called
while (true) {
boolean hasEvents = false;
try {
if (!close) {
//该方法遍历了eventqueue中所有的pollorEvent
//然后依次调用pollorEvent的run方法
//将socket注册到selector中
hasEvents = events();
if (wakeupCounter.getAndSet(-1) > 0) {
keyCount = selector.selectNow();
} else {
keyCount = selector.select(selectorTimeout);
}
wakeupCounter.set(0);
}
if (close) {
events();
timeout(0, false);
try {
selector.close();
} catch (IOException ioe) {
log.error(sm.getString("endpoint.nio.selectorCloseFail"), ioe);
}
break;
}
} catch (Throwable x) {
ExceptionUtils.handleThrowable(x);
log.error("",x);
continue;
}
//either we timed out or we woke up, process events first
if ( keyCount == 0 ) hasEvents = (hasEvents | events());

Iterator<SelectionKey> iterator =
keyCount > 0 ? selector.selectedKeys().iterator() : null;
// Walk through the collection of ready keys and dispatch
// any active event.
while (iterator != null && iterator.hasNext()) {
SelectionKey sk = iterator.next();
NioSocketWrapper attachment = (NioSocketWrapper)sk.attachment();
// Attachment may be null if another thread has called
// cancelledKey()
if (attachment == null) {
iterator.remove();
} else {
iterator.remove();
//处理(重点)
processKey(sk, attachment);
}
}//while
//process timeouts
timeout(keyCount,hasEvents);
}//while
getStopLatch().countDown();
}

# 2.Poller,处理processKey
protected void processKey(SelectionKey sk, NioSocketWrapper attachment) {
try {
if ( close ) {
cancelledKey(sk);
} else if ( sk.isValid() && attachment != null ) {
if (sk.isReadable() || sk.isWritable() ) {
if ( attachment.getSendfileData() != null ) {
processSendfile(sk,attachment, false);
} else {
unreg(sk, attachment, sk.readyOps());
boolean closeSocket = false;
// Read goes before write
//读事件
if (sk.isReadable()) {
//处理socket(创建worker,重点)
if (!processSocket(attachment, SocketEvent.OPEN_READ, true)) {
closeSocket = true;
}
}
//写事件
if (!closeSocket && sk.isWritable()) {
if (!processSocket(attachment, SocketEvent.OPEN_WRITE, true)) {
closeSocket = true;
}
}
if (closeSocket) {
cancelledKey(sk);
}
}
}
} else {
//invalid key
cancelledKey(sk);
}
} catch ( CancelledKeyException ckx ) {
cancelledKey(sk);
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
log.error("",t);
}
}

Poller线程主要用于以较少的资源轮询已连接套接字以保持连接,当数据可用时转给工作线程
通过对Poller的run方法跟踪,可以看到会创建SocketProcessor(worker)对socket的进一步处理。

  • SocketProcessor (worker) 处理socket过程。
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
java复制代码# 1.AbstractEndpoint的processSocket()方法
//处理socket
public boolean processSocket(SocketWrapperBase<S> socketWrapper,
SocketEvent event, boolean dispatch) {
try {
if (socketWrapper == null) {
return false;
}
//创建SocketProcessor处理器(worker)
SocketProcessorBase<S> sc = processorCache.pop();
if (sc == null) {
sc = createSocketProcessor(socketWrapper, event);
} else {
sc.reset(socketWrapper, event);
}
//执行
Executor executor = getExecutor();
if (dispatch && executor != null) {
executor.execute(sc);
} else {
sc.run();
}
} catch (RejectedExecutionException ree) {
getLog().warn(sm.getString("endpoint.executor.fail", socketWrapper) , ree);
return false;
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
// This means we got an OOM or similar creating a thread, or that
// the pool and its queue are full
getLog().error(sm.getString("endpoint.process.fail"), t);
return false;
}
return true;
}
  • SocketProcessor 的 doRun()分析。
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
java复制代码# 1.SocketProcessor的doRun()方法
protected class SocketProcessor extends SocketProcessorBase<NioChannel> {
public SocketProcessor(SocketWrapperBase<NioChannel> socketWrapper, SocketEvent event) {
super(socketWrapper, event);
}
@Override
protected void doRun() {
NioChannel socket = socketWrapper.getSocket();
SelectionKey key = socket.getIOChannel().keyFor(socket.getPoller().getSelector());
try {
int handshake = -1;

if (handshake == 0) {
SocketState state = SocketState.OPEN;
// Process the request from this socket
if (event == null) {
//处理socket(重点)
state = getHandler().process(socketWrapper, SocketEvent.OPEN_READ);
} else {
state = getHandler().process(socketWrapper, event);
}
if (state == SocketState.CLOSED) {
close(socket, key);
}
}
}
}
}

# 2.AbstractProtocol的ConnectionHandler的process()方法。(只贴出重点关注代码)
@Override
public SocketState process(SocketWrapperBase<S> wrapper, SocketEvent status) {
//拿到socket对象
S socket = wrapper.getSocket();
Processor processor = connections.get(socket);
if (processor == null) {
//创建Processor(重点)
processor = getProtocol().createProcessor();
register(processor);
}
//通过processor执行wrapper(重点)
state = processor.process(wrapper, status);
// Make sure socket/processor is removed from the list of current
// connections
connections.remove(socket);
release(processor);
return SocketState.CLOSED;
}

# 3.AbstractHttp11Protocol.createProcessor()方法。 创建Processor。
protected Processor createProcessor() {
//构建Http11Processor
Http11Processor processor = new Http11Processor(getMaxHttpHeaderSize(),
getAllowHostHeaderMismatch(), getRejectIllegalHeaderName(), getEndpoint(),
getMaxTrailerSize(), allowedTrailerHeaders, getMaxExtensionSize(),
getMaxSwallowSize(), httpUpgradeProtocols, getSendReasonPhrase(),
relaxedPathChars, relaxedQueryChars);
//设置adapter适配器(重点)
processor.setAdapter(getAdapter());

//默认的keepAlive情况下,每个socket处理的最多的 请求次数
processor.setMaxKeepAliveRequests(getMaxKeepAliveRequests());

//开启keepAlive的Timeout
processor.setConnectionUploadTimeout(getConnectionUploadTimeout());

//http当遇到文件上传时 默认超时时间(300*1000)
processor.setDisableUploadTimeout(getDisableUploadTimeout());

//当http请求的body size超过这个值时,通过gzip进行压缩
processor.setCompressionMinSize(getCompressionMinSize());

//http请求是否开启compression处理,gzip压缩
processor.setCompression(getCompression());
processor.setNoCompressionUserAgents(getNoCompressionUserAgents());
//http body里面的内容是“text/html,text/xml,text/plain”
//才会进行压缩处理
processor.setCompressibleMimeTypes(getCompressibleMimeTypes());
processor.setRestrictedUserAgents(getRestrictedUserAgents());

//最大的post处理尺寸的大小 4*1000
processor.setMaxSavePostSize(getMaxSavePostSize());
processor.setServer(getServer());
processor.setServerRemoveAppProvidedValues(getServerRemoveAppProvidedValues());
return processor;
}

# 4.通过processor执行wrapper(AbstractProcessorLight.process()方法)
@Override
public SocketState process(SocketWrapperBase<?> socketWrapper, SocketEvent status)
throws IOException {
SocketState state = SocketState.CLOSED;
Iterator<DispatchType> dispatches = null;
do {
if (dispatches != null) {
DispatchType nextDispatch = dispatches.next();
state = dispatch(nextDispatch.getSocketStatus());
} else if (status == SocketEvent.DISCONNECT) {
// Do nothing here, just wait for it to get recycled
} else if (isAsync() || isUpgrade() || state == SocketState.ASYNC_END) {
state = dispatch(status);
if (state == SocketState.OPEN) {
//执行service方法,处理socket(重点)
state = service(socketWrapper);
}
} else if (status == SocketEvent.OPEN_WRITE) {
// Extra write event likely after async, ignore
state = SocketState.LONG;
} else if (status == SocketEvent.OPEN_READ){
state = service(socketWrapper);
} else {
state = SocketState.CLOSED;
}
if (dispatches == null || !dispatches.hasNext()) {
// Only returns non-null iterator if there are
// dispatches to process.
dispatches = getIteratorAndClearDispatches();
}
} while (state == SocketState.ASYNC_END ||
dispatches != null && state != SocketState.CLOSED);
return state;
}

# 5.Http11Processor的service()方法(只保留重点代码)
@Override
public SocketState service(SocketWrapperBase<?> socketWrapper){
//通过adapter处理请求(重点)
getAdapter().service(request, response);
}

通过上面的代码分析,SocketProcessor 在处理socket对象,最终是通过调用getAdapter().service(request, response)方法处理。

  • 下面进入到getAdapter().service(request, response)方法分析。
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
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
java复制代码# 1.CoyoteAdapter的service()方法
@Override
public void service(org.apache.coyote.Request req, org.apache.coyote.Response res)
throws Exception { //接收到所有的请求
//转换request和response
Request request = (Request) req.getNote(ADAPTER_NOTES);
Response response = (Response) res.getNote(ADAPTER_NOTES);
if (request == null) {
//通过connector创建request和response
request = connector.createRequest();
request.setCoyoteRequest(req);
response = connector.createResponse();
response.setCoyoteResponse(res);
//link将request和response连接起来
request.setResponse(response);
response.setRequest(request);
// Set as notes
req.setNote(ADAPTER_NOTES, request);
res.setNote(ADAPTER_NOTES, response);
//设置URI的编码
req.getParameters().setQueryStringCharset(connector.getURICharset());
}
if (connector.getXpoweredBy()) {
response.addHeader("X-Powered-By", POWERED_BY);
}
boolean async = false;
boolean postParseSuccess = false;
req.getRequestProcessor().setWorkerThreadName(THREAD_NAME.get());

try {

// Parse and set Catalina and configuration specific
// request parameters
//在map里面解析业务请求(重要)
postParseSuccess = postParseRequest(req, request, res, response);
if (postParseSuccess) {
//设置异步支持(getContainer()拿到的是engine)
//check valves if we support async
request.setAsyncSupported(
connector.getService().getContainer().getPipeline().isAsyncSupported());
// Calling the container
//执行pipeline管道(invoke)standardEngineValve.invoke (重点)
connector.getService().getContainer().getPipeline().getFirst().invoke(
request, response);
}
if (request.isAsync()) {

} else {
//请求和响应完成
request.finishRequest();
response.finishResponse();
}
}
}

# 2.standardEngineValve.invoke()方法
@Override
public final void invoke(Request request, Response response)
throws IOException, ServletException {
//获取StandardHost对象
// Select the Host to be used for this Request
Host host = request.getHost();
if (host == null) {
response.sendError
(HttpServletResponse.SC_BAD_REQUEST,
sm.getString("standardEngine.noHost",
request.getServerName()));
return;
}
if (request.isAsyncSupported()) {
request.setAsyncSupported(host.getPipeline().isAsyncSupported());
}
//执行standardHostValue.invoke()方法(重点)
// Ask this Host to process this request
host.getPipeline().getFirst().invoke(request, response);
}

# 3.standardHostValue.invoke()方法
@Override
public final void invoke(Request request, Response response)
throws IOException, ServletException {
//获取standardContext
Context context = request.getContext();
if (context == null) {
response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
sm.getString("standardHost.noContext"));
return;
}
if (request.isAsyncSupported()) {
request.setAsyncSupported(context.getPipeline().isAsyncSupported());
}
boolean asyncAtStart = request.isAsync();
boolean asyncDispatching = request.isAsyncDispatching();
try {
context.bind(Globals.IS_SECURITY_ENABLED, MY_CLASSLOADER);
if (!asyncAtStart && !context.fireRequestInitEvent(request.getRequest())) {
return;
}
try {
if (!asyncAtStart || asyncDispatching) {
//执行standardContextValue.invoke()的方法(重点)
context.getPipeline().getFirst().invoke(request, response);
}
}
}
}

# 4.standardContextValue.invoke()方法
@Override
public final void invoke(Request request, Response response)
throws IOException, ServletException {
//获取StandardWrapper
// Select the Wrapper to be used for this Request
Wrapper wrapper = request.getWrapper();
if (wrapper == null || wrapper.isUnavailable()) {
response.sendError(HttpServletResponse.SC_NOT_FOUND);
return;
}

if (request.isAsyncSupported()) {
request.setAsyncSupported(wrapper.getPipeline().isAsyncSupported());
}
//执行standardWrapperValue.invoke()的方法(重点)
wrapper.getPipeline().getFirst().invoke(request, response);
}

# 5.standardWrapperValue.invoke()方法
//最终找到servlet处理
@Override
public final void invoke(Request request, Response response)
throws IOException, ServletException {

// Initialize local variables we may need
boolean unavailable = false;
Throwable throwable = null;
// This should be a Request attribute...
long t1=System.currentTimeMillis();
//增加请求次数,CAS
requestCount.incrementAndGet();
StandardWrapper wrapper = (StandardWrapper) getContainer();

//获取servlet对象
Servlet servlet = null;
Context context = (Context) wrapper.getParent();

// Check for the application being marked unavailable
if (!context.getState().isAvailable()) {
response.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE,
sm.getString("standardContext.isUnavailable"));
unavailable = true;
}

// Check for the servlet being marked unavailable
if (!unavailable && wrapper.isUnavailable()) {
container.getLogger().info(sm.getString("standardWrapper.isUnavailable",
wrapper.getName()));
long available = wrapper.getAvailable();
if ((available > 0L) && (available < Long.MAX_VALUE)) {
response.setDateHeader("Retry-After", available);
response.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE,
sm.getString("standardWrapper.isUnavailable",
wrapper.getName()));
} else if (available == Long.MAX_VALUE) {
response.sendError(HttpServletResponse.SC_NOT_FOUND,
sm.getString("standardWrapper.notFound",
wrapper.getName()));
}
unavailable = true;
}

//Servlet默认的是在第一次请求的时候实例化
// Allocate a servlet instance to process this request
try {
if (!unavailable) {
//获取不到,再分配加载一个
servlet = wrapper.allocate();
}
} catch (UnavailableException e) {
container.getLogger().error(
sm.getString("standardWrapper.allocateException",
wrapper.getName()), e);
long available = wrapper.getAvailable();
if ((available > 0L) && (available < Long.MAX_VALUE)) {
response.setDateHeader("Retry-After", available);
response.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE,
sm.getString("standardWrapper.isUnavailable",
wrapper.getName()));
} else if (available == Long.MAX_VALUE) {
response.sendError(HttpServletResponse.SC_NOT_FOUND,
sm.getString("standardWrapper.notFound",
wrapper.getName()));
}
} catch (ServletException e) {
container.getLogger().error(sm.getString("standardWrapper.allocateException",
wrapper.getName()), StandardWrapper.getRootCause(e));
throwable = e;
exception(request, response, e);
} catch (Throwable e) {
ExceptionUtils.handleThrowable(e);
container.getLogger().error(sm.getString("standardWrapper.allocateException",
wrapper.getName()), e);
throwable = e;
exception(request, response, e);
servlet = null;
}

//获取path
MessageBytes requestPathMB = request.getRequestPathMB();
DispatcherType dispatcherType = DispatcherType.REQUEST;
if (request.getDispatcherType()==DispatcherType.ASYNC) dispatcherType = DispatcherType.ASYNC;
request.setAttribute(Globals.DISPATCHER_TYPE_ATTR,dispatcherType);
request.setAttribute(Globals.DISPATCHER_REQUEST_PATH_ATTR,
requestPathMB);
// Create the filter chain for this request
ApplicationFilterChain filterChain =
ApplicationFilterFactory.createFilterChain(request, wrapper, servlet);

//执行相应的filter过滤器链
// Call the filter chain for this request
// NOTE: This also calls the servlet's service() method
try {
if ((servlet != null) && (filterChain != null)) {
// Swallow output if needed
if (context.getSwallowOutput()) {
try {
SystemLogHandler.startCapture();
if (request.isAsyncDispatching()) {
request.getAsyncContextInternal().doInternalDispatch();
} else {
filterChain.doFilter(request.getRequest(),
response.getResponse());
}
} finally {
String log = SystemLogHandler.stopCapture();
if (log != null && log.length() > 0) {
context.getLogger().info(log);
}
}
} else {
if (request.isAsyncDispatching()) {
request.getAsyncContextInternal().doInternalDispatch();
} else {
filterChain.doFilter
(request.getRequest(), response.getResponse());
}
}

}
} catch (ClientAbortException | CloseNowException e) {
if (container.getLogger().isDebugEnabled()) {
container.getLogger().debug(sm.getString(
"standardWrapper.serviceException", wrapper.getName(),
context.getName()), e);
}
throwable = e;
exception(request, response, e);
} catch (IOException e) {
container.getLogger().error(sm.getString(
"standardWrapper.serviceException", wrapper.getName(),
context.getName()), e);
throwable = e;
exception(request, response, e);
} catch (UnavailableException e) {
container.getLogger().error(sm.getString(
"standardWrapper.serviceException", wrapper.getName(),
context.getName()), e);
// throwable = e;
// exception(request, response, e);
wrapper.unavailable(e);
long available = wrapper.getAvailable();
if ((available > 0L) && (available < Long.MAX_VALUE)) {
response.setDateHeader("Retry-After", available);
response.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE,
sm.getString("standardWrapper.isUnavailable",
wrapper.getName()));
} else if (available == Long.MAX_VALUE) {
response.sendError(HttpServletResponse.SC_NOT_FOUND,
sm.getString("standardWrapper.notFound",
wrapper.getName()));
}
// Do not save exception in 'throwable', because we
// do not want to do exception(request, response, e) processing
} catch (ServletException e) {
Throwable rootCause = StandardWrapper.getRootCause(e);
if (!(rootCause instanceof ClientAbortException)) {
container.getLogger().error(sm.getString(
"standardWrapper.serviceExceptionRoot",
wrapper.getName(), context.getName(), e.getMessage()),
rootCause);
}
throwable = e;
exception(request, response, e);
} catch (Throwable e) {
ExceptionUtils.handleThrowable(e);
container.getLogger().error(sm.getString(
"standardWrapper.serviceException", wrapper.getName(),
context.getName()), e);
throwable = e;
exception(request, response, e);
}

// Release the filter chain (if any) for this request
if (filterChain != null) {
filterChain.release();
}

// Deallocate the allocated servlet instance
try {
if (servlet != null) {
wrapper.deallocate(servlet);
}
} catch (Throwable e) {
ExceptionUtils.handleThrowable(e);
container.getLogger().error(sm.getString("standardWrapper.deallocateException",
wrapper.getName()), e);
if (throwable == null) {
throwable = e;
exception(request, response, e);
}
}

// If this servlet has been marked permanently unavailable,
// unload it and release this instance
try {
if ((servlet != null) &&
(wrapper.getAvailable() == Long.MAX_VALUE)) {
wrapper.unload();
}
} catch (Throwable e) {
ExceptionUtils.handleThrowable(e);
container.getLogger().error(sm.getString("standardWrapper.unloadException",
wrapper.getName()), e);
if (throwable == null) {
throwable = e;
exception(request, response, e);
}
}
}

通过对CoyoteAdapter的service()方法分析,可以知道他是一步一步的调用方法。
StandardEngineValue–>StandardHostValue–>StandardContextValue–>StandardWrapperValue的invoke方法。

  • web请求的流程图:
  • 在这里插入图片描述
  • 附件:tomcat源码(有注释)github.com/llsydn/tomc…
  • tomcat类关系图
  • 在这里插入图片描述

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Tomcat8源码解析(一)

发表于 2021-08-09

Tomcat8源码解析

Tomcat总体架构

  • 在这里插入图片描述

Connector:开启Socket并监听客户端请求,返回响应数据;
Container:负责具体的请求处理;

一个Service负责维护多个Connector和一个Container,这样来自Connector的请求只能有它所属的Service维护的Container处理;

Engine:代表整个servlet引擎
Host:表示一个虚拟主机
Context:表示一个应用
wrapper:表示一个servlet

Tomcat源码搭建

  1. tomcat软件和源码文件下载链接:tomcat.apache.org/download-80…
  • 在这里插入图片描述
  1. 创建一个tomcat目录文件夹,存放下载的文件,然后创建一个pom.xml文件。然后使用idea打开该tomcat目录即可。
  • 在这里插入图片描述
  • pom.xml文件如下
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
java复制代码<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.apache.tomcat</groupId>
<artifactId>tomcat</artifactId>
<version>1.0-SNAPSHOT</version>

<name>tomcat</name>
<url>http://www.example.com</url>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.7</maven.compiler.source>
<maven.compiler.target>1.7</maven.compiler.target>
</properties>

<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.easymock</groupId>
<artifactId>easymock</artifactId>
<version>3.4</version>
</dependency>
<dependency>
<groupId>org.apache.ant</groupId>
<artifactId>ant</artifactId>
<version>1.10.1</version>
</dependency>
<dependency>
<groupId>wsdl4j</groupId>
<artifactId>wsdl4j</artifactId>
<version>1.6.2</version>
</dependency>
<dependency>
<groupId>javax.xml</groupId>
<artifactId>jaxrpc</artifactId>
<version>1.1</version>
</dependency>
<dependency>
<groupId>org.eclipse.jdt</groupId>
<artifactId>org.eclipse.jdt.core</artifactId>
<version>3.13.0</version>
</dependency>
<dependency>
<groupId>org.eclipse.jdt.core.compiler</groupId>
<artifactId>ecj</artifactId>
<version>4.5.1</version>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.6.1</version>
<configuration>
<encoding>UTF-8</encoding>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
  1. 在idea设置如下图所示:
  • 在这里插入图片描述
  • 在这里插入图片描述
  1. 最后启动即可。启动成功访问路径:http://localhost:8080/
  • 在这里插入图片描述

Tomcat源码分析

1.Tomcat初始化阶段
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
143
144
java复制代码# 1.Bootstrap启动类的main方法
public static void main(String args[]) {
if (daemon == null) {
//实例化BootStrap
Bootstrap bootstrap = new Bootstrap();
try {
//初始化BootStrap
bootstrap.init();
} catch (Throwable t) {
handleThrowable(t);
t.printStackTrace();
return;
}
daemon = bootstrap;
} else {
// When running as a service the call to stop will be on a new
// thread so make sure the correct class loader is used to prevent
// a range of class not found exceptions.
Thread.currentThread().setContextClassLoader(daemon.catalinaLoader);
}

try {
String command = "start";
if (args.length > 0) {
command = args[args.length - 1];
}

if (command.equals("startd")) {
args[args.length - 1] = "start";
daemon.load(args);
daemon.start();
} else if (command.equals("stopd")) {
args[args.length - 1] = "stop";
daemon.stop();
} else if (command.equals("start")) {
//当命令是start,执行下面操作
daemon.setAwait(true); //为了让tomcat在关闭端口阻塞监听关闭命令
daemon.load(args); //实际上调用catalina.load()方法,初始化server,service,engine,executor,connector
daemon.start(); //实际上调用catalina.start()方法,启动server,service,engine,executor,connector;Host,Context,Wrapper
if (null == daemon.getServer()) {
System.exit(1);
}
} else if (command.equals("stop")) {
daemon.stopServer(args);
} else if (command.equals("configtest")) {
daemon.load(args);
if (null == daemon.getServer()) {
System.exit(1);
}
System.exit(0);
} else {
log.warn("Bootstrap: command \"" + command + "\" does not exist.");
}
} catch (Throwable t) {
// Unwrap the Exception for clearer error reporting
if (t instanceof InvocationTargetException &&
t.getCause() != null) {
t = t.getCause();
}
handleThrowable(t);
t.printStackTrace();
System.exit(1);
}
}

# 2.初始化BootStrap
public void init() throws Exception {
//初始化类加载器
initClassLoaders();

//当前线程设置上下文类加载器
Thread.currentThread().setContextClassLoader(catalinaLoader);

//设置安全机制的类加载器
SecurityClassLoad.securityClassLoad(catalinaLoader);

// Load our startup class and call its process() method
if (log.isDebugEnabled())
log.debug("Loading startup class");
//加载Catalina类
Class<?> startupClass = catalinaLoader.loadClass("org.apache.catalina.startup.Catalina");
//使用Catalina类的构造方法,创建Catalina实例对象
Object startupInstance = startupClass.getConstructor().newInstance();

//执行Catalina的setParentClassLoader方法,设置父类加载器
if (log.isDebugEnabled())
log.debug("Setting startup class properties");
String methodName = "setParentClassLoader";
Class<?> paramTypes[] = new Class[1];
paramTypes[0] = Class.forName("java.lang.ClassLoader");
Object paramValues[] = new Object[1];
paramValues[0] = sharedLoader;
Method method =
startupInstance.getClass().getMethod(methodName, paramTypes);
method.invoke(startupInstance, paramValues);

//设置catalinaDaemon
catalinaDaemon = startupInstance;
}

# 3.初始化类加载器
private void initClassLoaders() {
try {
//commonLoader的父类加载器就设置为null,即打破了双亲委派机制。
commonLoader = createClassLoader("common", null);
if( commonLoader == null ) {
// no config file, default to this loader - we might be in a 'single' env.
commonLoader=this.getClass().getClassLoader();
}
//catalinaLoader和sharedLoader,的父类加载器设置为commonLoader
catalinaLoader = createClassLoader("server", commonLoader);
sharedLoader = createClassLoader("shared", commonLoader);
} catch (Throwable t) {
handleThrowable(t);
log.error("Class loader creation threw exception", t);
System.exit(1);
}
}

# 4.daemon.load(args);初始化操作
private void load(String[] arguments)
throws Exception {

// Call the load() method
String methodName = "load";
Object param[];
Class<?> paramTypes[];
if (arguments==null || arguments.length==0) {
paramTypes = null;
param = null;
} else {
paramTypes = new Class[1];
paramTypes[0] = arguments.getClass();
param = new Object[1];
param[0] = arguments;
}
//执行catalina的load()方法。
Method method =
catalinaDaemon.getClass().getMethod(methodName, paramTypes);
if (log.isDebugEnabled())
log.debug("Calling startup class " + method);
method.invoke(catalinaDaemon, param);

}

从上面的代码可以看出,tomcat的启动类入口是Bootstrap类的main方法。
可以看出这里有几个重要的步骤:
1.初始化Bootstrap,主要是反射创建catalina对象;初始化tomcat类加载器
2.调用Bootstrap的load方法,初始化tomcat相关组件,实际上是调用catalina的load方法。

  • 执行的catalina.load()方法,接着源码跟踪分析
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
java复制代码/**
* 会去初始化一些资源,优先加载conf/server.xml,找不到再去加载server-embed.xml;
* 此外,load方法还会初始化Server
*/
public void load() {
if (loaded) {
return;
}
loaded = true;
long t1 = System.nanoTime();

//初始化目录
initDirs();

//初始化命名空间
initNaming();

//解析器,解析Server.xml文件
Digester digester = createStartDigester();

//读取Server.xml文件
InputSource inputSource = null;
InputStream inputStream = null;
File file = null;
try {
try {
file = configFile();
inputStream = new FileInputStream(file);
inputSource = new InputSource(file.toURI().toURL().toString());
} catch (Exception e) {
if (log.isDebugEnabled()) {
log.debug(sm.getString("catalina.configFail", file), e);
}
}

try {
inputSource.setByteStream(inputStream);
digester.push(this);

//开始解析Server.xml文件(重点)
digester.parse(inputSource);
}
}

//设置server的Catalina等信息
getServer().setCatalina(this);
getServer().setCatalinaHome(Bootstrap.getCatalinaHomeFile());
getServer().setCatalinaBase(Bootstrap.getCatalinaBaseFile());

// Stream redirection
initStreams();

// Start the new server
try {

//初始化Server,这个init方法是父类LifecycleBase的方法(重点)
getServer().init();
} catch (LifecycleException e) {
if (Boolean.getBoolean("org.apache.catalina.startup.EXIT_ON_INIT_FAILURE")) {
throw new java.lang.Error(e);
} else {
log.error("Catalina.start", e);
}
}
long t2 = System.nanoTime();
if(log.isInfoEnabled()) {
log.info("Initialization processed in " + ((t2 - t1) / 1000000) + " ms");
}
}

catalina.load方法,主要是做了2个重要的操作:
1.解析server.xml文件。
2.初始化Server。

  • 在这里插入图片描述
  • 执行的getServer().init()方法初始化Server,源码分析
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
java复制代码# 1.LifecycleBase的init()方法
@Override
public final synchronized void init() throws LifecycleException {
if (!state.equals(LifecycleState.NEW)) {
invalidTransition(Lifecycle.BEFORE_INIT_EVENT);
}

try {
setStateInternal(LifecycleState.INITIALIZING, null, false);
//调用子类的initInternal方法(重要)
initInternal();
setStateInternal(LifecycleState.INITIALIZED, null, false);
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
setStateInternal(LifecycleState.FAILED, null, false);
throw new LifecycleException(
sm.getString("lifecycleBase.initFail",toString()), t);
}
}

# 2.StandardServer的initInternal()方法
@Override
protected void initInternal() throws LifecycleException {

super.initInternal();

// Register global String cache
// Note although the cache is global, if there are multiple Servers
// present in the JVM (may happen when embedding) then the same cache
// will be registered under multiple names
onameStringCache = register(new StringCache(), "type=StringCache");

//注册JMX
// Register the MBeanFactory
MBeanFactory factory = new MBeanFactory();
factory.setContainer(this);
onameMBeanFactory = register(factory, "type=MBeanFactory");

// Register the naming resources
globalNamingResources.init();

//获取类加载器
// Populate the extension validator with JARs from common and shared
// class loaders
if (getCatalina() != null) {
ClassLoader cl = getCatalina().getParentClassLoader();
// Walk the class loader hierarchy. Stop at the system class loader.
// This will add the shared (if present) and common class loaders
while (cl != null && cl != ClassLoader.getSystemClassLoader()) {
if (cl instanceof URLClassLoader) {
URL[] urls = ((URLClassLoader) cl).getURLs();
for (URL url : urls) {
if (url.getProtocol().equals("file")) {
try {
File f = new File (url.toURI());
if (f.isFile() &&
f.getName().endsWith(".jar")) {
ExtensionValidator.addSystemResource(f);
}
} catch (URISyntaxException e) {
// Ignore
} catch (IOException e) {
// Ignore
}
}
}
}
cl = cl.getParent();
}
}
// Initialize our defined Services
for (int i = 0; i < services.length; i++) {
//初始化services(重点)
services[i].init();
}
}

Server的初始化init()方法,会先调用父类LifecycleBase的init()方法,然后再调用子类Server的initInternal()方法
这个调用的模式:父类init—>子类initInternal,在后面的services,connector,engine初始化是一样的模式。

  • services[i].init(),初始化services,源码分析
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
java复制代码# 1.standardService的initInternal()方法
@Override
protected void initInternal() throws LifecycleException {

super.initInternal();

//初始化engine(重点)
if (engine != null) {
engine.init();
}

//初始化线程池
// Initialize any Executors
for (Executor executor : findExecutors()) {
if (executor instanceof JmxEnabled) {
((JmxEnabled) executor).setDomain(getDomain());
}
executor.init();
}

//初始化mapper映射监听器
// Initialize mapper listener
mapperListener.init();

// Initialize our defined Connectors
synchronized (connectorsLock) {
for (Connector connector : connectors) {
try {
//初始化connector(重点)
connector.init();
} catch (Exception e) {
String message = sm.getString(
"standardService.connector.initFailed", connector);
log.error(message, e);

if (Boolean.getBoolean("org.apache.catalina.startup.EXIT_ON_INIT_FAILURE"))
throw new LifecycleException(message);
}
}
}
}

从services的初始化分析,可以得到services的初始化,会初始化engine和connector。(executor、mapperListener)

  • engine.init(),初始化engine,源码分析
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
java复制代码# 1.standardEngine的initInternal()方法
@Override
protected void initInternal() throws LifecycleException {
// Ensure that a Realm is present before any attempt is made to start
// one. This will create the default NullRealm if necessary.
getRealm();
super.initInternal();
}

# 2.ContainerBase的initInternal()方法
@Override
protected void initInternal() throws LifecycleException {
BlockingQueue<Runnable> startStopQueue = new LinkedBlockingQueue<>();
startStopExecutor = new ThreadPoolExecutor(
getStartStopThreadsInternal(),
getStartStopThreadsInternal(), 10, TimeUnit.SECONDS,
startStopQueue,
new StartStopThreadFactory(getName() + "-startStop-"));
startStopExecutor.allowCoreThreadTimeOut(true);
super.initInternal();
}

# 3.LifecycleMBeanBase的initInternal()方法
@Override
protected void initInternal() throws LifecycleException {
// If oname is not null then registration has already happened via
// preRegister().
if (oname == null) {
mserver = Registry.getRegistry(null, null).getMBeanServer();
oname = register(this, getObjectNameKeyProperties());
}
}
  • connector.init(),初始化connector,源码分析
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
java复制代码# 1.connector的initInternal()方法
@Override
protected void initInternal() throws LifecycleException {

super.initInternal();

//初始化适配器
adapter = new CoyoteAdapter(this);
protocolHandler.setAdapter(adapter);

//给parseBodyMethodsSet设置一个默认值
if (null == parseBodyMethodsSet) {
setParseBodyMethods(getParseBodyMethods());
}

if (protocolHandler.isAprRequired() && !AprLifecycleListener.isAprAvailable()) {
throw new LifecycleException(sm.getString("coyoteConnector.protocolHandlerNoApr",
getProtocolHandlerClassName()));
}
if (AprLifecycleListener.isAprAvailable() && AprLifecycleListener.getUseOpenSSL() &&
protocolHandler instanceof AbstractHttp11JsseProtocol) {
AbstractHttp11JsseProtocol<?> jsseProtocolHandler =
(AbstractHttp11JsseProtocol<?>) protocolHandler;
if (jsseProtocolHandler.isSSLEnabled() &&
jsseProtocolHandler.getSslImplementationName() == null) {
// OpenSSL is compatible with the JSSE configuration, so use it if APR is available
jsseProtocolHandler.setSslImplementationName(OpenSSLImplementation.class.getName());
}
}

try {
//初始化protocolHandler(重点)
protocolHandler.init();
} catch (Exception e) {
throw new LifecycleException(
sm.getString("coyoteConnector.protocolHandlerInitializationFailed"), e);
}
}

从connector的初始化分析,可以得到connector的初始化,会初始化protocolHandler

  • protocolHandler.init(),初始化protocolHandler,源码分析
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
java复制代码# 1.AbstractProtocol的init()方法
@Override
public void init() throws Exception {
if (getLog().isInfoEnabled()) {
getLog().info(sm.getString("abstractProtocolHandler.init", getName()));
}

if (oname == null) {
// Component not pre-registered so register it
oname = createObjectName();
if (oname != null) {
Registry.getRegistry(null, null).registerComponent(this, oname, null);
}
}

if (this.domain != null) {
rgOname = new ObjectName(domain + ":type=GlobalRequestProcessor,name=" + getName());
Registry.getRegistry(null, null).registerComponent(
getHandler().getGlobal(), rgOname, null);
}

String endpointName = getName();
endpoint.setName(endpointName.substring(1, endpointName.length()-1));
endpoint.setDomain(domain);

//初始化endpoint(重点)
endpoint.init();
}

从protocolHandler的初始化分析,可以得到protocolHandler的初始化,会初始化endpoint

  • endpoint.init(),初始化endpoint,源码分析
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码# 1.AbstractEndpoint的init()方法
public void init() throws Exception {
if (bindOnInit) {
bind();
bindState = BindState.BOUND_ON_INIT;
}
if (this.domain != null) {
// Register endpoint (as ThreadPool - historical name)
oname = new ObjectName(domain + ":type=ThreadPool,name=\"" + getName() + "\"");
Registry.getRegistry(null, null).registerComponent(this, oname, null);

ObjectName socketPropertiesOname = new ObjectName(domain +
":type=ThreadPool,name=\"" + getName() + "\",subType=SocketProperties");
socketProperties.setObjectName(socketPropertiesOname);
Registry.getRegistry(null, null).registerComponent(socketProperties, socketPropertiesOname, null);

for (SSLHostConfig sslHostConfig : findSslHostConfigs()) {
registerJmx(sslHostConfig);
}
}
}

到此tomcat的初始化阶段就已经完成了。使用的是责任链模式,一步一步的初始化。
组件初始化的顺序:
Server–>Service–>Engine–>Connector–>ProtocolHandler–>Endpoint
可以看到,Host,Context,Wrapper还没有开始初始化。这些将在tomcat的start启动阶段初始化。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Docker 系列】docker 学习七,DockerFil

发表于 2021-08-09

这是我参与8月更文挑战的第9天,活动详情查看:8月更文挑战

【Docker 系列】docker 学习六,DockerFile

我们开始来一起学习 DockerFile 的知识点

DcokerFile 是用来构建 docker 镜像的文件,是一个命令参数脚本

一般 docker 镜像的构建步骤:

1、编写一个 dockerfile 文件

2、docker build 构建成为一个镜像

3、docker run 运行镜像

4、docker push 发布镜像(咱们可以发布到 DockerHub,也可以发布到阿里云上面)

我们来看看官方的镜像是咋玩的

例如我们在 DockerHub 上搜索 ubuntu ,看看官网的 DockerFile 是啥样子的

hub.docker.com/_/ubuntu

点击链接我们会进入到 git 仓库上,也是 DockerFile

咱们看到就 3 行 Docker 命令,是官方做成镜像的 DockerFile,所以这个官方的 ubuntu 镜像是非常简单的,阉割版本的,甚至连 clear 命令都没有,ll命令也没有

很多的官方镜像包都是非常简单的,很多功能都是没有的,我们通常会自己搭建自己的镜像来满足我们的各种需求

DockerFile 的构建过程

官方能构建镜像,我们也可以自己的镜像

DockerFile 的基础知识:

  • 每个 DockerFile 的保留字(指令),都必须是大写的
  • DockerFile 脚本执行是按照顺序执行的
  • # 表示注释
  • 每一个指令都会创建提交一个新的镜像层,并提交

可以在网络上找到这样的图片,可以看到镜像是一层一层的,可以在浏览器上搜索到 DockerFile 里面的指令解释

dockerfile 是面向开发的,我们以后在做项目的时候,是直接发布一个镜像,交付的是一个镜像,就需要编写 DockerFile 文件,这个文件非常的简单!

咱们必须要掌握 Docker 镜像,逐渐成为企业交付的标准了。

咱们学习的过程是先会使用别人的东西,再去研究别人是怎么写的,进而我们也学会如何去写,去开发

例如:

我们先学习使用了,

DockerImages:通过 DockerFile 构建生产的镜像,最终发布和运行的产品

Docker 容器:容器服务就是镜像运行起来的服务器

现在我们开始详细学习 DockerFIle : 构建文件,定义了一切的步骤,这是源代码

DockerFile 的指令

图片来源于网络,我们一一解释一波

  • FROM

基础的镜像,一切都是从这里开始的

  • MAINTAINER

指明镜像是谁写的,写下自己的姓名和邮箱

  • RUN

镜像构建的时候需要运行的命令

  • ADD

加入某些配置,例如加入 mysql 的压缩包,添加内容

  • WORKDIR

镜像的工作目录

  • VOLUME

挂载目录

  • EXPOSE

暴露端口 和 -p 是一个效果

  • CMD

指定这个容器启动的时候执行的命令,只会是最优一个指令进行生效,会被替代

  • ENTRYPOINT

指定这个容器启动的时候执行的命令,可以追加

  • ONBUILD

当构建一个被继承的 DockerFIle ,这个时候就会运行 ONBUILD 的指令,触发相应的动作

  • COPY

与 ADD 类似,此命令是将文件拷贝到镜像中

  • ENV

构建的时候设置环境变量

乍一看感觉 CMD 和 ENTRYPOINT功能好像差不多,但是还是不太清楚具体区别在哪里,文章末尾会有详细说明

实战

我们自己来做一个自定一个 ubuntu 镜像

官方的ubuntu是阉割版本的,很多工具和命令都是不支持的,那么我们就自己加进去,自给自足

自己写一个 DockerFile

这里需要注意的是,基本上 99%的镜像,都是基于这个基础镜像 scratch,我们可以看到官方的 DockerFIle 也是基于这个镜像来玩的

ubuntu git url

那么我们可以基于这个 ubuntu 来进行自定义,加入一些我们需要的工具,如vim,ifconfig等

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
dockerfile复制代码FROM ubuntu

RUN apt-get update # 更新源

RUN apt-get install -y vim # 安装 vim

RUN apt-get install -y net-tools # 安装 net-tools

ENV MYPATH /usr/local # 设置环境变量
WORKDIR $MYPATH # 设置镜像工作目录

EXPOSE 8888 # 暴露端口

CMD echo "----- end -----" # 执行 echo 命令

CMD /bin/bash

开始构建

docker build -f dockerfile2 -t xmtubuntu .

如果不在 DockerFile 中写入 apt-get update 更新源,会出现下面这个问题,这个要注意

执行上述命令,会看到如下打印信息,最终会看到Successfully,即为构建成功

通过上图我们可以看出, DokerFile 中写了 9 个步骤,执行的时候也是分了 9 步,知道全部成功才算成功

最终构建成功后我们可以看到

1
2
复制代码Successfully built a6f88c9f245b
Successfully tagged xmtubuntu:latest

验证结果

docker images 查看我们的镜像

1
2
3
shell复制代码# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
xmtubuntu latest a6f88c9f245b 13 minutes ago 172MB

docker inspect a6f88c9f245b 查看我们镜像的构建过程

1
2
3
4
5
6
7
8
9
10
11
12
shell复制代码# docker history a6f88c9f245b
IMAGE CREATED CREATED BY SIZE COMMENT
a6f88c9f245b 14 minutes ago /bin/sh -c #(nop) CMD ["/bin/sh" "-c" "/bin… 0B
3c0d23b8188f 14 minutes ago /bin/sh -c #(nop) CMD ["/bin/sh" "-c" "echo… 0B
ffb019142fc7 14 minutes ago /bin/sh -c #(nop) EXPOSE 8888 0B
8867e6d97670 14 minutes ago /bin/sh -c #(nop) WORKDIR /usr/local 0B
c9d0141ec3b0 14 minutes ago /bin/sh -c #(nop) ENV MYPATH=/usr/local 0B
41e73f7e314d 14 minutes ago /bin/sh -c apt-get install -y net-tools 1.52MB
52013ca51f1d 14 minutes ago /bin/sh -c apt-get install -y vim 68.2MB
5ea7d553d403 14 minutes ago /bin/sh -c apt-get update 29.7MB
1318b700e415 11 days ago /bin/sh -c #(nop) CMD ["bash"] 0B
<missing> 11 days ago /bin/sh -c #(nop) ADD file:524e8d93ad65f08a0… 72.8MB

xmtubuntu 这个镜像的构建过程我们可以清晰的看出都执行了哪些步骤,当然,同样的方式,我们也可以看看官方的镜像是如何构建的,我们来看看官方 ubuntu 的

1
2
3
4
shell复制代码# docker history ubuntu
IMAGE CREATED CREATED BY SIZE COMMENT
1318b700e415 11 days ago /bin/sh -c #(nop) CMD ["bash"] 0B
<missing> 11 days ago /bin/sh -c #(nop) ADD file:524e8d93ad65f08a0… 72.8MB

官方的就很简单,阉割了很多东西,我们可以看出官方的 ubuntu 就 2 个步骤,第一个是加入ubuntu 压缩包,第二个就是 /bin/bash

我们查看我们的自定义镜像 xmtubuntu

果然,我们的自定义ubuntu镜像,有了vim,ifconfig工具,实战成功

CMD 和 ENTRYPOINT 的区别

  • CMD

指定这个容器启动的时候执行的命令,只会是最优一个指令进行生效,会被替代

  • ENTRYPOINT

指定这个容器启动的时候执行的命令,可以追加

如何理解呢?我们来做一个对比试验就可以很好的理解上述的解释说明,docker 里面有很多命令会有这样的微小区别,我们可以举一反三,慢慢深入学习

CMD 的例子

写一个简单的 DockerFile 文件名为 dockerfile-cmd

1
2
dockerfile复制代码FROM xmtubuntu
CMD ["ls","-a"]

构建镜像

1
2
3
4
5
6
7
8
9
10
shell复制代码e# docker build -f dockerfile-cmd -t dockerfile-cmd .
Sending build context to Docker daemon 1.346GB
Step 1/2 : FROM xmtubuntu
---> a6f88c9f245b
Step 2/2 : CMD ["ls","-a"]
---> Running in 101670af4290
Removing intermediate container 101670af4290
---> 1697fc03b8ce
Successfully built 1697fc03b8ce
Successfully tagged dockerfile-cmd:latest

创建并启动容器

docker run 101670af4290,可以看到如下效果

image-20210807121235380

我们尝试在启动容器时候追加命令

docker run 101670af4290 -l,就会有如下报错

1
2
shell复制代码# docker run 1697fc03b8ce -l
docker: Error response from daemon: OCI runtime create failed: container_linux.go:380: starting container process caused: exec: "-l": executable file not found in $PATH: unknown.

原因如下:

使用 CMD指令是(例如我们的例子是 ls -a),我们在启动容器的时候,后面追加的命令(-l)会把 ls -a替换掉,由于-l不是一个命令,因此报错

ENTRYPOINT 的例子

写一个简单的 DockerFile 文件名为 dockerfile-entrypoint

1
2
dockerfile复制代码FROM xmtubuntu
ENTRYPOINT ["ls","-a"]

构建镜像,创建并启动容器和 CMD 的例子一模一样,咱们直接启动容器的效果和 CMD的例子也是一模一样,我们直接来看启动容器并追加参数的例子

可以看出使用 ENTRYPOINT是可以在后面追加参数的,使用CMD若指令后面追加参数,那么会覆盖CMD指定的指令

那么,对于以后遇到相关的指令,我们也可以举一反三,做对比试验,这样我们就会理解的更加清楚

如何发布我们的镜像

1、登录 dockerhub

没有注册的 xdm 可以注册一个,hub.docker.com/

1
2
3
4
5
6
7
shell复制代码# docker login -u xxxx用户名
Password:
WARNING! Your password will be stored unencrypted in /root/.docker/config.json.
Configure a credential helper to remove this warning. See
https://docs.docker.com/engine/reference/commandline/login/#credentials-store

Login Succeeded

2、修改我们的镜像 tag

docker tag 我们的镜像id 我们的docker用户名/镜像名字:版本

3、将镜像推到我们自己的仓库中

发布镜像的时候,也是按照一层一层的提交的

最后补充一个网络上找到的图片,现在看这张图就能更清晰的明白其中的原理了

参考资料:

docker docs

欢迎点赞,关注,收藏

朋友们,你的支持和鼓励,是我坚持分享,提高质量的动力

好了,本次就到这里

技术是开放的,我们的心态,更应是开放的。拥抱变化,向阳而生,努力向前行。

我是小魔童哪吒,欢迎点赞关注收藏,下次见~

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

sed替换路径字符串遇到的问题 sed替换路径等参数时遇到的

发表于 2021-08-08

sed替换路径等参数时遇到的问题

问题描述

写脚本需要替换json串中的路径 脚本如下:

1
2
3
bash复制代码path=/root/baixw/test
​
sed -i "s/path=.*/path=$path/g" ./sed_path.txt

脚本需要替换文本中的带有路径的字符串,结果脚本执行出现错误:

1
vbnet复制代码sed: -e expression #1, char 26: unknown option to 's'

使用bash -x调试脚本发现sed命令所在行被解析为:

sed -i "s/path=.*/path=/root/baixw/test' ./sed_path.txt

很明显了,String中有特殊字符,需要用``转义,故最后转义的结果真滴是眼睛都花了,还是不好处理啊

解决办法

经过百度,得到如下解决办法:

1
2
3
bash复制代码path=/root/baixw/test
​
sed -i "s?path=.*?path=$path?g" ./sed_path.txt

sed命令中有如下设计:当用户不方便转义字符串中的特殊字符(如/)时,sed支持使用自定义分隔符,让用户避开冲突

即可以定义不会冲突的字符即可,如上,我所替换的即为?作为分隔符,也是可以很完美的即决问题的

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Spring Boot 回顾(八):让你的注入更加灵活--

发表于 2021-08-08

这是我参与8月更文挑战的第8天,活动详情查看:8月更文挑战

前言

上一篇给大家介绍了@Valid注解,帮助大家更好的应该繁琐的字段校验场景。这一篇给大家介绍另一个实用的注解@ConditionalOn*。大家在开发的过程中一定遇到过这些场景:当满足一定条件时,我们才去实例化bean;当满足上下文存在时,才去实例化bean;或者说在特性的java版本时我们才去实例化bean。类似上述的场景还有很多,之前我们可能采用的方式就是先把bean统统注入进来,然后通过一些if判断来选择我们需要的对象。相信大家看完本篇以后一定有更多的选择来应对以上的场景。

@Conditional

其实我们在之前用spring框架时,有使用到类似的注解,那就是@Conditional。@Conditional是Spring4新提供的注解,它的作用是按照一定的条件进行判断,满足条件给容器注册bean。源码如下

1
2
3
4
5
6
java复制代码@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Conditional {
Class<? extends Condition>[] value();
}

我们再看下Condition接口

1
2
3
java复制代码public interface Condition {
boolean matches(ConditionContext var1, AnnotatedTypeMetadata var2);
}

可以看到,我们就是利用matches进行条件匹配,满足就注入,不满足就不执行。
那么我们再来看看Spring Boot下的@ConditionalOn*注解。首先看下Spring Boot中有哪些这样的注解。
image.png
可以看到这里一共提供了13不同场景下的@ConditionalOn*注解供我们使用,下面我们一一简单进行介绍。

@ConditionalOn*

我们将Spring Boot提供的@ConditionalOn*注解整理成表格,方便大家参考使用。

序号 注解名称 使用场景
1 @ConditionalOnBean 仅仅在当前上下文中存在某个对象时,才会实例化一个Bean
2 @ConditionalOnClass 某个class位于类路径上,才会实例化一个Bean
3 @ConditionalOnCloudPlatform 只有运行在指定的云平台上才加载指定的 bean
4 @ConditionalOnExpression 当表达式为true的时候,才会实例化一个Bean
5 @ConditionalOnJava 只有运行指定版本的 Java 才会加载 Bean
6 @ConditionalOnJndi 只有指定的资源通过 JNDI 加载后才加载 bean
7 @ConditionalOnMissionBean 仅仅在当前上下文中不存在某个对象时,才会实例化一个Bean
8 @ConditionalOnMIssionClass 某个class类路径上不存在的时候,才会实例化一个Bean
9 @ConditionalOnNotWebAppilcation 在非 web 环境才加载 bean
10 @ConditionalOnProperty 通过配置文件中的属性值来判定configuration是否被注入
11 @ConditionalOnResource 当要加载的 bean 依赖指定资源是否存在于 classpath 中,那么我们就可以使用这个注解
12 @ConditionalOnSingleCandidate 只有指定类已存在于 BeanFactory 中,并且可以确定单个候选项才会匹配成功
13 @ConditionalOnWebApplication 只有运行在 web 应用里才会加载这个 bean

总结

当然,除了以上提供的13个不同注解,我们还可以使用组合的方式将不用的注解放到一起使用,通过运用条件and、or让我们可以应该更加复杂的场景。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…572573574…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%