本节继续对activiti流程引擎的配置进行学习
1、EventLog配置
1.1、配置EventLog
首先在activiti_eventlog.cfg.xml中配置eventlog属性为true
1.1.1测试代码
编写一个eventlog测试代码 ConfigEventLogTest.java
import org.activiti.engine.event.EventLogEntry;
import org.activiti.engine.runtime.ProcessInstance;
import org.activiti.engine.task.Task;
import org.activiti.engine.test.ActivitiRule;
import org.activiti.engine.test.Deployment;
import org.junit.Rule;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
/**
* Describe:
*
* @author cwqsolo
* @date 2020/01/07
*/
public class ConfigEventLogTest {
private static final Logger logger = LoggerFactory.getLogger(ConfigEventLogTest.class);
//这里已经包含了流程引擎的创建
@Rule
public ActivitiRule activitiRule = new ActivitiRule("activiti_eventlog.cfg.xml");
@Test
@Deployment(resources = {"./my-process.bpmn20.xml"})
public void configMDCTest1() {
ProcessInstance processInstance = activitiRule.getRuntimeService().startProcessInstanceByKey("my-process");
List<Task> list = activitiRule.getTaskService().createTaskQuery().list();
activitiRule.getTaskService().complete(list.get(0).getId());
List<EventLogEntry> eventLogEntries = activitiRule.getManagementService()
.getEventLogEntriesByProcessInstanceId(processInstance.getProcessInstanceId());
for (EventLogEntry eventLogEntry : eventLogEntries) {
logger.info("eventlog.type= {}, eventlog.data ={}", eventLogEntry.getType().toString(), new String( eventLogEntry.getData()) );
}
logger.info("eventlogEntryies size={}", eventLogEntries.size());
}
}
1.2 执行日志
执行日志情况如下:
源码查看
在ProcessEngineConfigurationImpl.java类中有创建时间监听器的代码
这里创建了一个新的EventLogger对象,这个对象是实现了一个监听器
在这个类的初始化函数里面,设置了各种事件的监听处理类
另外,当监听到事件后,调用 onEvent方法:
2、 事件与监听器原理
2.1 配置监听器
有三种类型的监听器
eventListeners:监听所有事件派发的通知
typedEventListeners:监听指定事件类型的通知
activiti:eventListener:只监听特定流程定义的事件
相关api对象:
ActivitiEvent 事件对象
ActivitiEventListener 监听器
ActivitiEventType 事件类型,是枚举类型,监听器针对具体的类型进行操作
2.2 通过eventListener监听事件
首先在配置文件中添加监听的配置
生成一个事件监听类的具体实现ProcessEventListener.java
package com.study.activiti.event;
import org.activiti.engine.delegate.event.ActivitiEvent;
import org.activiti.engine.delegate.event.ActivitiEventListener;
import org.activiti.engine.delegate.event.ActivitiEventType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Describe:
*
* @author cwqsolo
* @date 2020/01/09
*/
public class ProcessEventListener implements ActivitiEventListener {
private static final Logger logger = LoggerFactory.getLogger(ProcessEventListener.class);
@Override
public void onEvent(ActivitiEvent event) {
ActivitiEventType type = event.getType();
if( ActivitiEventType.PROCESS_STARTED.equals( type)){
logger.info("流程启动 {} \t 流程实例id={}", type, event.getProcessInstanceId());
}
if( ActivitiEventType.PROCESS_COMPLETED.equals( type)){
logger.info("流程启动 {} \t 流程实例id={}", type, event.getProcessInstanceId());
}
}
@Override
public boolean isFailOnException() {
return false;
}
}
创建测试代码ConfigEventListenerTest
import com.study.activiti.event.CustomEventListener;
import org.activiti.engine.delegate.event.ActivitiEventType;
import org.activiti.engine.delegate.event.impl.ActivitiActivityEventImpl;
import org.activiti.engine.event.EventLogEntry;
import org.activiti.engine.runtime.ProcessInstance;
import org.activiti.engine.task.Task;
import org.activiti.engine.test.ActivitiRule;
import org.activiti.engine.test.Deployment;
import org.junit.Rule;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List;
/**
* Describe:
*
* @author cwqsolo
* @date 2020/01/07
*/
public class ConfigEventListenerTest {
private static final Logger logger = LoggerFactory.getLogger(ConfigEventListenerTest.class);
//这里已经包含了流程引擎的创建
@Rule
public ActivitiRule activitiRule = new ActivitiRule("activiti_eventListener.cfg.xml");
@Test
@Deployment(resources = {"./my-process.bpmn20.xml"})
public void test1() {
ProcessInstance processInstance = activitiRule.getRuntimeService().startProcessInstanceByKey("my-process");
List<Task> list = activitiRule.getTaskService().createTaskQuery().list();
activitiRule.getTaskService().complete(list.get(0).getId());
List<EventLogEntry> eventLogEntries = activitiRule.getManagementService()
.getEventLogEntriesByProcessInstanceId(processInstance.getProcessInstanceId());
for (EventLogEntry eventLogEntry : eventLogEntries) {
logger.info("eventlog.type= {}, eventlog.data ={}", eventLogEntry.getType().toString(), new String( eventLogEntry.getData()) );
}
logger.info("eventlogEntryies size={}", eventLogEntries.size());
}
}
执行日志中会打印出来刚才监听实现中的打印信息
2.3 通过eventTypeListener监听事件
下面在上面基础上添加eventTpyeListener的功能。这个功能是通过监听指定类型的事件作出响应
创建针对指定类型的事件监听类ProcessEventTypeListener.java
package com.study.activiti.event;
import org.activiti.engine.delegate.event.ActivitiEvent;
import org.activiti.engine.delegate.event.ActivitiEventListener;
import org.activiti.engine.delegate.event.ActivitiEventType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Describe:
*
* @author cwqsolo
* @date 2020/01/09
*/
public class ProcessEventTypeListener implements ActivitiEventListener {
private static final Logger logger = LoggerFactory.getLogger(ProcessEventTypeListener.class);
@Override
public void onEvent(ActivitiEvent event) {
ActivitiEventType type = event.getType();
if( ActivitiEventType.ACTIVITY_COMPLETED .equals( type)){
logger.info("活动结束 {} \t 流程实例id={}", type, event.getProcessInstanceId());
}
}
@Override
public boolean isFailOnException() {
return false;
}
}
在配置文件中添加对应的配置内容
执行的时候,我们可以看到在原来的监听基础上,我们还多了事件类型的监听处理
2.4 监听用户自定义事件
我们还可以进行用户自定义的监听,需要3个步骤:
首先添加一个事件监听的自定义处理
流程事件处理中dispatch一个自定义事件
在配置文件中添加这个事件的处理配置
下面我们看一下上面的三个步骤具体实现
- 创建一个类CustomEventListener.java
package com.study.activiti.event;
import org.activiti.engine.delegate.event.ActivitiEvent;
import org.activiti.engine.delegate.event.ActivitiEventListener;
import org.activiti.engine.delegate.event.ActivitiEventType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Describe:
*
* @author cwqsolo
* @date 2020/01/09
*/
public class CustomEventListener implements ActivitiEventListener {
private static final Logger logger = LoggerFactory.getLogger(CustomEventListener.class);
@Override
public void onEvent(ActivitiEvent event) {
ActivitiEventType type = event.getType();
if( ActivitiEventType.CUSTOM.equals( type)){
logger.info("监听到用户事件 {} \t 流程实例id={}", type, event.getProcessInstanceId());
}
}
@Override
public boolean isFailOnException() {
return false;
}
}
2 修改流程处理中内容,在测试代码中添加下面语句
3、修改配置文件
最后我们看一下执行结果
2.5 源码中事件监听的层级
3、命令拦截器
3.1 创建一个拦截器
在下列位置创建一个拦截器
这个类的代码如下:
package com.study.activiti.interceptor;
import org.activiti.engine.impl.interceptor.AbstractCommandInterceptor;
import org.activiti.engine.impl.interceptor.Command;
import org.activiti.engine.impl.interceptor.CommandConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Describe:
*
* @author cwqsolo
* @date 2020/01/13
*/
public class DurationCommandInterceptor extends AbstractCommandInterceptor {
private static final Logger logger = LoggerFactory.getLogger(DurationCommandInterceptor.class);
@Override
public <T> T execute(CommandConfig config, Command<T> command) {
long start = System.currentTimeMillis();
try{
return this.getNext().execute(config, command);
}finally{
long duration = System.currentTimeMillis()- start;
logger.info("{} 执行时长 {} 毫秒", command.getClass().getSimpleName(), duration);
}
}
}
3.2 创建配置文件:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="processEngineConfiguration"
class="org.activiti.engine.impl.cfg.StandaloneInMemProcessEngineConfiguration">
<property name="dataSource" ref="dataSource"></property> <!-- dataSource bean -->
<property name="databaseSchemaUpdate"
value="false"></property> <!--activiti这个属性可以进行库表创建 true, false , create-drop -->
<property name="commandInvoker" ref="commandInvoker"/>
<property name="customPreCommandInterceptors">
<list>
<bean class="com.study.activiti.interceptor.DurationCommandInterceptor"/>
</list>
</property>
</bean>
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName"
value="com.mysql.jdbc.Driver"></property> <!-- 数据库驱动类 mysql是这个,其它的数据库修改一下即可 -->
<property name="url"
value="jdbc:mysql://localhost:3306/activiti6?characterEncoding=UTF-8"></property> <!-- 数据库URL,我放在名为activiti数据库中 -->
<property name="username" value="root"></property> <!-- 连接数据库的账号 -->
<property name="password" value="root123"></property> <!-- 连接数据库的密码 -->
<property name="initialSize" value="1"></property> <!-- -->
<property name="maxActive" value="20"></property> <!-- -->
<property name="filters" value="stat,slf4j"></property> <!-- -->
</bean>
<!--将我们自定义的流程引擎打印信息用的拦截器设置到流程引擎里面 -->
<bean id="commandInvoker" class="com.study.activiti.interceptor.MDCCommandInvoke"/>
</beans>
3.3 创建测视类
创建一个测试类ConfigInterceptorTest.java
import org.activiti.engine.delegate.event.ActivitiEventType;
import org.activiti.engine.delegate.event.impl.ActivitiActivityEventImpl;
import org.activiti.engine.event.EventLogEntry;
import org.activiti.engine.runtime.ProcessInstance;
import org.activiti.engine.task.Task;
import org.activiti.engine.test.ActivitiRule;
import org.activiti.engine.test.Deployment;
import org.junit.Rule;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List;
/**
* Describe:
*
* @author cwqsolo
* @date 2020/01/07
*/
public class ConfigInterceptorTest {
private static final Logger logger = LoggerFactory.getLogger(ConfigInterceptorTest.class);
//这里已经包含了流程引擎的创建
@Rule
public ActivitiRule activitiRule = new ActivitiRule("activiti_interceptor.cfg.xml");
@Test
@Deployment(resources = {"./my-process.bpmn20.xml"})
public void test1() {
ProcessInstance processInstance = activitiRule.getRuntimeService().startProcessInstanceByKey("my-process");
List<Task> list = activitiRule.getTaskService().createTaskQuery().list();
activitiRule.getTaskService().complete(list.get(0).getId());
}
}
3.4 执行结果
下图为执行日志
4、作业执行器
作业执行器的相关配置有:
作业执行器的配置
配置自定义线程池
流程定义定时启动配置
一些重要的配置参数:
asyncExecutorActivate 激活作业执行器,asyncExecutorXXX 异步执行配置,asyncExecutor, 一步执行器器bean
自定义线程池 ExecutorService: corePoolSize核心线程数 maxPoolSize最大线程数
QueueCapacity:堵塞队列大小
作业执行定时开始事件(Timer Start Event):
timeDate, 指定启动时间
timeDuration: 指定持续时间间隔后执行
timeCycle:指定周期执行
下面我们实现一个基于job的流程,并进行监听,打印job执行的相关信息
4.1 创建一个定时执行流程定义文件
首先创建一个bpmc的xml文件
<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:activiti="http://activiti.org/bpmn"
xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:omgdc="http://www.omg.org/spec/DD/20100524/DC"
xmlns:omgdi="http://www.omg.org/spec/DD/20100524/DI" typeLanguage="http://www.w3.org/2001/XMLSchema"
expressionLanguage="http://www.w3.org/1999/XPath" targetNamespace="http://www.activiti.org/test">
<process id="my-process">
<!-- <startEvent id="start" />-->
<startEvent id="start" >
<timerEventDefinition>
<timeCycle>R5/PT10S</timeCycle>
</timerEventDefinition>
</startEvent>
<sequenceFlow id="flow1" sourceRef="start" targetRef="someTask" />
<userTask id="someTask" name="Activiti is awesome!" />
<sequenceFlow id="flow2" sourceRef="someTask" targetRef="end" />
<endEvent id="end" />
</process>
</definitions>
4.2 创建指定JOB监听器
创建一个只监听job的监听器:JobEventListener.java
package com.study.activiti.event;
import org.activiti.engine.delegate.event.ActivitiEvent;
import org.activiti.engine.delegate.event.ActivitiEventListener;
import org.activiti.engine.delegate.event.ActivitiEventType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Describe:
*
* @author cwqsolo
* @date 2020/01/09
*/
public class JobEventListener implements ActivitiEventListener {
private static final Logger logger = LoggerFactory.getLogger(JobEventListener.class);
@Override
public void onEvent(ActivitiEvent event) {
ActivitiEventType type = event.getType();
String name = type.name();
if( name.startsWith("TIMER") || name.startsWith("JOB")){
logger.info("监听到用户事件 {} \t 流程实例id={}", type, event.getProcessInstanceId());
}
}
@Override
public boolean isFailOnException() {
return false;
}
}
4.3 创建配置文件
创建对应的配置文件activiti-job.cfg.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"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="processEngineConfiguration"
class="org.activiti.engine.impl.cfg.StandaloneInMemProcessEngineConfiguration">
<property name="dataSource" ref="dataSource"></property> <!-- dataSource bean -->
<property name="databaseSchemaUpdate"
value="false"></property> <!--activiti这个属性可以进行库表创建 true, false , create-drop -->
<property name="enableDatabaseEventLogging" value="true" />
<property name="asyncExecutorActivate" value="true"></property> <!--打开系统自带的异步线程池 -->
<property name="asyncExecutor" ref="asyncExecutor"/>
<property name="eventListeners">
<list>
<bean class="com.study.activiti.event.JobEventListener"/>
</list>
</property>
</bean>
<bean id="asyncExecutor" class="org.activiti.engine.impl.asyncexecutor.DefaultAsyncJobExecutor">
<property name="executorService" ref="executorService"/>
</bean>
<bean id="executorService" class ="org.springframework.scheduling.concurrent.ThreadPoolExecutorFactoryBean">
<property name="threadNamePrefix" value="act-job-"/>
<property name="corePoolSize" value="5"/>
<property name="maxPoolSize" value="20"/>
<property name="queueCapacity" value="100"/>
<property name="rejectedExecutionHandler">
<bean class="java.util.concurrent.ThreadPoolExecutor$AbortPolicy"/>
</property>
</bean>
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName"
value="com.mysql.jdbc.Driver"></property> <!-- 数据库驱动类 mysql是这个,其它的数据库修改一下即可 -->
<property name="url"
value="jdbc:mysql://localhost:3306/activiti6?characterEncoding=UTF-8"></property> <!-- 数据库URL,我放在名为activiti数据库中 -->
<property name="username" value="root"></property> <!-- 连接数据库的账号 -->
<property name="password" value="root123"></property> <!-- 连接数据库的密码 -->
<property name="initialSize" value="1"></property> <!-- -->
<property name="maxActive" value="20"></property> <!-- -->
<property name="filters" value="stat,slf4j"></property> <!-- -->
</bean>
<!--将我们自定义的流程引擎打印信息用的拦截器设置到流程引擎里面 -->
<bean id="commandInvoker" class="com.study.activiti.interceptor.MDCCommandInvoke" />
</beans>
4.4 创建测试类
创建一个测视类,主要是打印定时job启动后的任务执行信息,任务执行信息是通过监听器来传递的。
import org.activiti.engine.event.EventLogEntry;
import org.activiti.engine.runtime.Job;
import org.activiti.engine.runtime.ProcessInstance;
import org.activiti.engine.task.Task;
import org.activiti.engine.test.ActivitiRule;
import org.activiti.engine.test.Deployment;
import org.junit.Rule;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List;
/**
* Describe:
*
* @author cwqsolo
* @date 2020/01/07
*/
public class ConfigJobTest {
private static final Logger logger = LoggerFactory.getLogger(ConfigJobTest.class);
//这里已经包含了流程引擎的创建
@Rule
public ActivitiRule activitiRule = new ActivitiRule("activiti_job.cfg.xml");
@Test
@Deployment(resources = {"./my-process-job.bpmn20.xml"})
public void Test1() throws InterruptedException {
logger.info("start");
List<Job> jobs = activitiRule.getManagementService().createTimerJobQuery().listPage(0, 100);
for (Job job:jobs){
logger.info("定时任务={}, 默认重试次数={}", job, job.getRetries());
}
logger.info("jobs.size={}", jobs.size());
Thread.sleep(1000*100);
logger.info("end");
}
}
4.5 执行结果
在监听器中,我们代码指定了如果事件的类似为TIMER或者JOB的需要进行处理(打印)
5、与Spring集成
集成spring需要添加pom依赖activi-spring,基于Spring的默认配置是activiti-context.xml,核心服务需要注入Spring容器。如果需要单元测试,还需要在pom中添加spring-test,辅助测试Rule:ActivitiRule。下面通过一个demo来演示如何进行spring集成。思路是:先集成spring,然后通过自定义个bean,注入到流程的某个环节中。
5.1 创建一下配置文件
创建一个配置文件activiti-context.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"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName"
value="com.mysql.jdbc.Driver"></property> <!-- 数据库驱动类 mysql是这个,其它的数据库修改一下即可 -->
<property name="url"
value="jdbc:mysql://localhost:3306/activiti6?characterEncoding=UTF-8"></property> <!-- 数据库URL,我放在名为activiti数据库中 -->
<property name="username" value="root"></property> <!-- 连接数据库的账号 -->
<property name="password" value="root123"></property> <!-- 连接数据库的密码 -->
<property name="initialSize" value="1"></property> <!-- -->
<property name="maxActive" value="20"></property> <!-- -->
<property name="filters" value="stat,slf4j"></property> <!-- -->
</bean>
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<!--指定流程引擎配置对象 -->
<bean id="processEngineConfiguration"
class="org.activiti.spring.SpringProcessEngineConfiguration">
<property name="dataSource" ref="dataSource"></property> <!-- dataSource bean -->
<property name="transactionManager" ref="transactionManager"/>
<property name="databaseSchemaUpdate" value="true"></property>
</bean>
<!-- 指定流程引擎配置对象工厂bean-->
<bean id="processEngine" class="org.activiti.spring.ProcessEngineFactoryBean">
<property name="processEngineConfiguration" ref="processEngineConfiguration"/>
</bean>
<!--服务注入 -->
<bean id="runtimeService" factory-bean="processEngine" factory-method="getRuntimeService"/>
<bean id="repositoryService" factory-bean="processEngine" factory-method="getRepositoryService"/>
<bean id="formService" factory-bean="processEngine" factory-method="getFormService"/>
<bean id="taskService" factory-bean="processEngine" factory-method="getTaskService"/>
<bean id="historyService" factory-bean="processEngine" factory-method="getHistoryService"/>
<bean id="activitiRule" class="org.activiti.engine.test.ActivitiRule">
<property name="processEngine" ref="processEngine"></property>
</bean>
</beans>
5.2 创建测试类
创建一个测试类可以进行spring集成测试
import org.activiti.engine.RuntimeService;
import org.activiti.engine.TaskService;
import org.activiti.engine.runtime.ProcessInstance;
import org.activiti.engine.task.Task;
import org.activiti.engine.test.ActivitiRule;
import org.activiti.engine.test.Deployment;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
/**
* Describe:
*
* @author cwqsolo
* @date 2020/01/07
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"classpath:activiti_context.xml"})
public class ConfigSpringTest {
private static final Logger logger = LoggerFactory.getLogger(ConfigSpringTest.class);
//这里自动装配配置文件中的activitirule
@Rule
@Autowired
public ActivitiRule activitiRule;
@Autowired
private RuntimeService runtimeService;
@Autowired
private TaskService taskService;
@Test
@Deployment(resources = {"./my-process.bpmn20.xml"})
public void test() {
ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("my-process");
;
assertNotNull(processInstance);
List<Task> list = taskService.createTaskQuery().list();
for (Task task : list
) {
taskService.complete(task.getId());
}
}
}
5.3 执行日志
下图是执行日志的情况:
注意一下,spring需要junit的版本在4.12以上,否则会报告如下错误。
5.4 设计自定义bean
下面设计一个自定义的bean,并注入到流程执行过程中。在流程配置文件中添加一个节点,并且这个节点执行这个bean的sayHello方法。创建一个类HelloBean
package com.study.activiti.delegate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Describe:
*
* @author cwqsolo
* @date 2020/01/29
*/
public class HelloBean {
private static final Logger logger = LoggerFactory.getLogger(HelloBean.class);
public void sayHello(){
logger.info("Hello world");
}
}
在配置文件activiti_context.xml里配置bean的地方,加上这个bean的配置
5.5 创建一个新的流程定义
下面创建一个新的流程定义文件,在某个环节中配置上我们的自定义类
5.6 流程结合自定义bean执行日志
执行我们的测试类,在打印的信息中可以看到下面内容: