camunda流程引擎——Java集成Camunda(上)(笔记)

目录

  • 一、以一个处理流程开始
    • 1.1 后端
    • 1.2 前端
    • 1.3 执行
  • 二、Camunda的补充
    • 2.1 使用方式
    • 2.2 可视化平台的Cockpit
    • 2.3 流程相关数据
    • 2.4 表介绍
    • 2.5 前端集成Modeler
  • 三、用Java集成Camunda
    • 3.1 集成配置
    • 3.2 自动部署
      • 3.2.1 修改process.xml位置
      • 3.2.2 多进程引擎配置与多租户
    • 3.3 历史事件配置
      • 3.3.1 查询
      • 3.3.2 任务报告
    • 3.4 Service
    • 3.5 用户业务
    • 3.6 流程启动Controller
    • 3.7 业务任务-内部任务
      • 3.7.1 Java Class实现
      • 3.7.2 Delegate Expression实现
      • 3.7.3 Express实现
      • 3.7.4 流程的回退与重启与暂停
        • 回退
        • 重启
        • 暂停
    • 3.8 业务任务-外部任务
      • 3.8.1 异步响应:长轮询(Long Polling)
      • 3.8.2 注解方式
        • 配置与依赖
        • 写一个外部任务
        • 重试方式
        • 优先级
    • 3.9 任务监听器(引擎端)
    • 3.10 鉴权
    • 3.11 多实例任务
      • 3.11.1 顺序执行
      • 3.11.2 多实例内置变量
      • 3.11.3 并行执行
    • 3.12 脚本任务
  • 附录

集成与简单的使用请参看前文。camunda流程引擎基本使用(笔记)

流程活动可参考-流程引擎实施参考

例子学习于blibli-camunda工作流实战课程,该视频简介者有一些地方是错的,谨慎观看。

一、以一个处理流程开始

我们建立如下流程,安排:

  • 后端:加入购物车与物流发货。使用SpringBoot配合Camunda的包
  • 前端:付款(只有逻辑)。使用nodejs代码配合Camuanda的包(也可以用VUE等方式)
    在这里插入图片描述

1.1 后端

后端配置如下内容:

server:port: 8081camunda:connect:url: "http://localhost:8080/engine-rest"timeout: 10000

注意,笔者现在使用的是7.2版本,和上篇文章版本不同(可不替换)。

<?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>org.example</groupId><artifactId>CamudaDemo</artifactId><version>1.0-SNAPSHOT</version><properties><maven.compiler.source>17</maven.compiler.source><maven.compiler.target>17</maven.compiler.target></properties><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.7.6</version></parent><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></dependency><dependency><groupId>javax.xml.bind</groupId><artifactId>jaxb-api</artifactId></dependency><dependency><groupId>org.camunda.bpm</groupId><artifactId>camunda-external-task-client</artifactId><version>7.20.0</version></dependency></dependencies>
</project>

创建一个config,配置Camunda连接

package com.camunda.demo.config;import lombok.Data;
import org.camunda.bpm.client.ExternalTaskClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;@Configuration
@Data
public class CamundaConfig {/*** platform地址* */@Value("${camunda.connect.url}")private String CAMUNDA_URL;/*** 超时时间* */@Value("${camunda.connect.timeout}")private Integer TIME_OUT;@Beanpublic ExternalTaskClient getExternalTaskClient(){return  ExternalTaskClient.create().baseUrl(CAMUNDA_URL).asyncResponseTimeout(TIME_OUT).build();}
}

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

package com.camunda.demo.shopping;import lombok.extern.slf4j.Slf4j;
import org.camunda.bpm.client.ExternalTaskClient;
import org.camunda.bpm.engine.variable.Variables;
import org.springframework.stereotype.Component;import javax.annotation.Resource;
import java.util.Map;@Slf4j
@Component(value = "shoppingTask")
public class ShoppingTask {@Resourceprivate ExternalTaskClient externalTaskClient;/*** 订阅一个Service(external类型)* */public void handleShoppingCart(){/*** 订阅服务* */externalTaskClient.subscribe("add_card")/*** 流程定义,即所属流程不* */.processDefinitionKey("Process_11089zy")/*** 最长加锁时间* */.lockDuration(2000)/*** 订阅配置* ===========* 处理区域** externalTask 外部传参相关** externalTaskService 流程执行相关* */.handler((externalTask,externalTaskService)->{/*** 处理逻辑* */log.info("开始加入到购物车");Map<String,Object> goodVariable = Variables.createVariables().putValue("size","xl").putValue("count",2);log.info("已加入到购物车:\n{}",goodVariable);/*** 执行完成* */externalTaskService.complete(externalTask,goodVariable);}).open();}public void handleDelivery(){externalTaskClient.subscribe("delivery").processDefinitionKey("Process_11089zy").lockDuration(2000).handler(((externalTask, externalTaskService) -> {Object toWhere = externalTask.getVariable("toWhere");log.info("即将发往目的地:{}",toWhere);externalTaskService.complete(externalTask);})).open();}}

可以临时写一个启动类

@SpringBootApplication
public class MainApplication {public static void main(String[] args) {ConfigurableApplicationContext configurableApplicationContext = SpringApplication.run(MainApplication.class,args);ShoppingTask shoppingTask = (ShoppingTask)configurableApplicationContext.getBean("shoppingTask");shoppingTask.handleShoppingCart();shoppingTask.handleDelivery();}
}

注解方式也类似,见后文 外部任务。

1.2 前端

使用如下命令,初始化前端

 npm init -y

使用如下命令,安装依赖(如果有版本问题,可以加–force,要注意找一个低版本的,高版本不支持直接启动(笔者使用2.2.0)

可以参考-camunda-external-task-client-js

npm install camunda-external-task-client-js
npm install -D open

直接写一个subscribe.js
在这里插入图片描述

在这里插入图片描述

const { Client, logger ,Variables} = require("camunda-external-task-client-js");/*** 流程包* ==========* 配置项* */
const config = {baseUrl: "http://localhost:8080/engine-rest",use:logger,asyncResponseTimeout: 10000}const client = new Client(config);/*** 配置项* ========* 流程处理** 订阅服务-同后端代码* */
client.subscribe('pay',{processDefinitionKey:"Process_11089zy"},async function({task,taskService}){/*** 读取我们加入购物车流程中的变量* */const size = task.variables.get('size');const count = task.variables.get('count');console.log(`顾客下单尺寸:${size},数量${count}`);/*** 添加新的变量* 订单送往哪里* */const processVariables = new Variables().set("toWhere","shanghai China")try{/*** 处理完成* 查看源码可知:compelete 返回的是Promise,因此需要await来等待* */await taskService.complete(task,processVariables);console.log("完成");}catch (e){console.error(`异常${e}`);}})

通过如下代码直接启动

node .\subscribe.js

1.3 执行

完成后,我们部署这个流程
在这里插入图片描述
登录可以看到我们的流程已经部署上去了,启动它。(基本使用见上一篇)
在这里插入图片描述

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

在这里插入图片描述

二、Camunda的补充

2.1 使用方式

  1. 嵌入式:及整合进项目,如上篇中的Java整合Camunda。代码与流程解耦。
  2. 组件式:被所有程序共有,类似一个消息队列。它将任务指派给不同程序。(一个调度组件)。代码与流程耦合。
  3. 中间件:做成一个平台,单独部署,第一部分就是这种方式(SAAS平台)。代码与流程解耦。
  4. 集群:camunda8,中间件集群。
  5. 云原生:camunda8,部署于k8s中,elasticsearch存储方式,以支持大型微服务。

2.2 可视化平台的Cockpit

可参考官方文档-Web Applications

我们启动platform平台时,可以进入到该页面。

在这里插入图片描述

  • Cockpit:监控中心(驾驶舱)
  • Tasklist:有权限可看到的任务
  • Admin:权限信息

点击进入Cockpit
在这里插入图片描述

  • Running Process Instance:流程实例
  • Open Incidents:异常
  • Open Human Task:人工任务
  • Deployed:部署的流程(流程定义)
    • Process Definitions:BPMN相关(流程)。 Cockpit 允许监控 BPMN 流程。仪表板是 BPMN 监视功能的入口点。选择已部署的流程定义或搜索流程实例。这将带您进入流程定义视图或流程实例视图。
    • Decision Definitions:DMN相关(决策)。Cockpit 允许监控 DMN 决策。仪表板是 DMN 监控功能的入口点。选择已部署的决策定义以访问决策定义视图。显示的有关决策定义和决策实例的所有数据都基于历史数据。与流程定义和流程实例不同,没有运行时数据,因为决策是即时执行的,没有中间等待状态或保存点。
    • Case Definitions: CMMN相关(案例,此功能仅包含在 Camunda 平台的企业版中,在社区版中不可用)。Cockpit允许监控CMMN案例。仪表板是 CMMN 监视功能的入口点。选择已部署的案例定义,或搜索案例实例。这将分别将您带到案例定义视图或案例实例视图。
    • Deployments:总部署(多版本,前面的多版本统一模型算一个)。显示所有部署、其资源以及这些资源的内容的概述。它允许删除现有部署,以及重新部署旧资源和创建新部署。可以显示部署中的资源内容。也可以从此视图下载单个资源

2.3 流程相关数据

在这里插入图片描述

进入某一个流程定义,我们是可以看到下列数据:

  • Deinition Version: 版本,默认每次修改再部署时,都会提高一个版本
  • Version Tag:版本标签,用户自定义版本
  • Definition Key:流程图的唯一ID。
  • Definition ID = Definition Key:Deinition Version:随机乱码。流程图下各个版本的唯一ID。
Definition ID 与 Definition Key 为 多 对 1
乱码的作用:同一流程图的同一版本可以交给不同租户(理解为公司),乱码就是为了区分不同租户,避免区分不开。
  • History Time to LIve:历史数据保存时间
  • Tenant ID:租户ID
  • Deployment ID:该Definition ID对应在数据库中的主键ID。
  • Definition Name:流程名称

流程实例
在这里插入图片描述

ID为流程实例的instance ID ,我们点进去

在这里插入图片描述

  • Instance ID:实例ID
  • Business Key:业务Key(流水号之类的)

其他部分

在这里插入图片描述

  • Variable:流程变量,以Map的形式,我们之前也操作过的
  • Incidents:异常
  • Called Process Instance:跨流程调用,调用其他实例
  • User Tasks:用户任务
  • Jobs:定时任务,可重做
  • External Task:外部任务

2.4 表介绍

关于Camunda的表可以参考camunda数据库表结构介绍
以及官方文档Database Schema

2.5 前端集成Modeler

参考官网-基于 Web 的 BPMN、DMN 和表单工具

三、用Java集成Camunda

集成可以参考

Spring Boot 集成

流程引擎配置

3.1 集成配置

<?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>org.example</groupId><artifactId>CamundaEngine</artifactId><version>1.0-SNAPSHOT</version><properties><maven.compiler.source>17</maven.compiler.source><maven.compiler.target>17</maven.compiler.target></properties><dependencyManagement><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-dependencies</artifactId><version>3.1.1</version><type>pom</type><scope>import</scope></dependency></dependencies></dependencyManagement><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></dependency><dependency><groupId>com.mysql</groupId><artifactId>mysql-connector-j</artifactId><scope>runtime</scope></dependency><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.5.1</version></dependency><dependency><groupId>org.camunda.bpm.springboot</groupId><artifactId>camunda-bpm-spring-boot-starter</artifactId><version>7.20.0</version><exclusions><exclusion><groupId>org.camunda.bpm.model</groupId><artifactId>camunda-cmmn-model</artifactId></exclusion></exclusions></dependency><dependency><groupId>org.camunda.bpm.model</groupId><artifactId>camunda-cmmn-model</artifactId><version>7.18.0</version></dependency><dependency><groupId>org.camunda.bpm.springboot</groupId><artifactId>camunda-bpm-spring-boot-starter-webapp</artifactId><version>7.20.0</version></dependency><dependency><groupId>org.camunda.bpm.springboot</groupId><artifactId>camunda-bpm-spring-boot-starter-rest</artifactId><version>7.20.0</version></dependency></dependencies>
</project>

注意:

  1. 7.20.0版本中camunda-cmmn-model这个包笔者导不进来,因此另外导入了一个版本
  2. 导入了webapp前端方便操作,页面默认位置如下:
http://localhost:8080/camunda/app/welcome/default/#!/login
  1. 引入了RestAPI。相关文档可参考REST API Reference

yml文件配置可以参考官方给出的

server:port: 8080
spring:datasource:url: jdbc:mysql://localhost:3306/camunda?useUnicode=true&NamePatternMatchesAll=true&characterEncoding=UTF-8&allowPublicKeyRetrieval=true&useSSL=falseusername: rootpassword: rootdriver-class-name: com.mysql.cj.jdbc.Driverapplication:name:   work-engine
camunda:bpm:admin-user:id: demopassword: demo
mybatis:configuration:log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

3.2 自动部署

启动时自动扫描文件

参考-官方文档-The processes.xml Deployment Descriptor

自定义部署(运行中添加)可以参考:【Camunda 三】Camunda模型文件部署


resources下,创建如下文件 ![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/c189109a043f4314b797049c9f671bc8.png#pic_center)

process.xml默认配置如下:

<?xml version="1.0" encoding="UTF-8"?>
<process-applicationxmlns="http://www.camunda.org/schema/1.0/ProcessApplication"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
><process-archive name="loan-approval"><process-engine>default</process-engine><properties><property name="isDeleteUponUndeploy">false</property><property name="isScanForProcessDefinitions">true</property></properties></process-archive></process-application>
  • process-archive name:这个是归档名称,可以为空
  • process-engine:部署到的流程引擎名称 ,可不填写默认(多引擎时需要)
  • isDeleteUponUndeploy:此属性控制取消部署流程应用程序是否应导致从数据库中删除流程引擎部署。默认设置为 false。如果此属性设置为 true,那么取消部署流程应用程序将导致从数据库中删除部署(包括流程实例)。
  • isScanForProcessDefinitions:如果此属性设置为 true,那么将自动扫描流程应用程序的类路径以查找可部署资源。

可以参考Camunda动态生成工作流流程定义并部署更新流程(新手上路版)

3.2.1 修改process.xml位置

如果需要修改process.xml位置,可以使用官方给出代码,如下:

@ProcessApplication(name="my-app",deploymentDescriptors={"path/to/my/processes.xml"}
)
public class MyProcessApp extends ServletProcessApplication {}

3.2.2 多进程引擎配置与多租户

如何创建多进程引擎可以参考The Process Application class,有多种配置方式

<process-application
xmlns="http://www.camunda.org/schema/1.0/ProcessApplication"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"><process-engine name="my-engine"><configuration>org.camunda.bpm.engine.impl.cfg.StandaloneInMemProcessEngineConfiguration</configuration></process-engine><process-archive name="loan-approval" tenantId="tenant1><process-engine>my-engine</process-engine><properties><property name="isDeleteUponUndeploy">false</property><property name="isScanForProcessDefinitions">true</property></properties></process-archive></process-application>

3.3 历史事件配置

可参考-历史记录和审核事件日志

在这里插入图片描述

通过配置该项,决定记录的日志颗粒度。

  • NONE:不触发任何历史记录事件
  • ACTIVITY:触发以下事件:
    • 流程实例 START、UPDATE、END、MIGRATE:在流程实例启动、更新、结束和迁移时触发
    • 案例实例 CREATE、UPDATE、CLOSE:在创建、更新和关闭案例实例时触发
    • 活动实例 START、UPDATE、END、MIGRATE:在活动实例启动、更新、结束和迁移时触发
    • 案例活动实例 CREATE、UPDATE、END:在创建、更新和结束案例活动实例时触发
    • 任务实例 CREATE、UPDATE、COMPLETE、DELETE、MIGRATE:在创建、更新(即重新分配、委派等)、完成、删除和迁移任务实例时触发。
  • AUDIT:除了 history level 提供的事件外,还会触发以下事件:ACTIVITY
    • 变量实例 CREATE、UPDATE、DELETE、MIGRATE:在创建、更新、删除和迁移流程变量时触发。默认历史记录后端 (DbHistoryEventHandler) 将变量实例事件写入历史变量实例数据库表。此表中的行会随着变量实例的更新而更新,这意味着只有流程变量的最后一个值可用。
    • FULL:除了 history level 提供的事件外,还会触发以下事件:AUDIT
      • 表单属性 UPDATE:在创建和/或更新表单属性时触发。
        默认历史记录后端 (DbHistoryEventHandler) 将历史变量更新写入数据库。这样就可以使用历史记录服务检查过程变量的中间值。
        用户操作日志更新:当用户执行声明用户任务、委派用户任务等操作时触发。
      • 事件创建、删除、解决、迁移:在创建、删除、解决和迁移事件时触发
      • 历史作业日志 CREATE、FAILED、SUCCESSFUL、DELETED:在创建作业、作业执行失败或成功或作业已删除时触发
      • 决策实例 EVALUATE:当 DMN 引擎评估决策时触发。
      • Batch START、END:在批处理开始和结束时触发
      • 标识链接 ADD、DELETE:在添加、删除标识链接时,或者在设置或更改用户任务的受托人时,以及设置或更改用户任务的所有者时触发。
      • 历史外部任务日志 CREATED, DELETED, FAILED, SUCCESSFUL:在已创建、删除外部任务或外部任务执行已报告失败或成功时触发。
  • AUTO:如果您计划在同一数据库上运行多个引擎,则该级别非常有用。在这种情况下,所有引擎都必须使用相同的历史记录级别。与其手动保持配置同步,不如使用级别,引擎会自动确定数据库中已配置的级别。如果未找到,则使用默认值。
    • 请记住:如果您计划使用自定义历史记录级别,则必须为每个配置注册自定义级别,否则会引发异常。

请注意,使用默认历史记录后端时,历史记录级别存储在数据库中,以后无法更改。

设置方式:

ProcessEngine processEngine = ProcessEngineConfiguration.createProcessEngineConfigurationFromResourceDefault().setHistory(ProcessEngineConfiguration.HISTORY_FULL).buildProcessEngine();
  • 使用Spring XML或部署描述符(bpm-platform.xml,processes.xml)进行设置。

  • 使用 Camunda Wildfly 子系统时,可以通过 Wildfly 配置(独立 .xml、domain.xml)设置属性。

总之,只需要在xm中添加如下成员变量即可

<property name="history">audit</property>

3.3.1 查询

使用HistoryService可创建如下查询:

  • HistoricProcessInstanceQuery:历史进程实例查询
  • HistoricCaseInstanceQuery:历史案例实例查询
  • HistoricActivityInstanceQuery:历史活动实例查询
  • HistoricCaseActivityInstanceQuery:历史案例活动查询
  • HistoricVariableInstanceQuery:历史变量查询
  • HistoricDetailQuery:历史细节查询,记录变量更新等信息
  • HistoricTaskInstanceQuery:任务实例查询
  • HistoricIncidentQuery:历史事件(异常)查询
  • UserOperationLogQuery:用户操作日志查询
  • HistoricJobLogQuery:历史作业日志查询
  • HistoricDecisionInstanceQuery:历史决策实例查询
  • HistoricBatchQuery:历史批量处理查询
  • HistoricIdentityLinkLogQuery:与用户关联的日志查询
  • HistoricExternalTaskLogQuery:外部日志查询

3.3.2 任务报告

检索已完成任务的报告。
对于任务报告,有两种可能的报告类型:

  • 计数
  • 持续时间。

3.4 Service

Camunda提供各类Service以便使用。

  • ProcessEngine:第一次调用流程引擎时初始化并构建流程引擎,之后总是返回相同的流程引擎

    • 可以使用ProcessEngines.init() 和 ProcessEngines.destroy()来正确地创建和关闭所有的流程引擎
    • 通过ProcessEngine.XXService 来获取Service
    • SpringBoot中不需要如此
  • RepositoryService:用于管理和操作部署和流程定义的操作

  • RuntimeService:流程实例相关操作

  • TaskService:Activity,也就是对各个节点的相关操作(含挂起,激活等)

  • IdentityService:用户管理等相关操作

  • FormService:表单相关操作

    • 启动表单是在流程实例启动之前向用户显示的表单
    • 任务表单是在用户希望完成表单时显示的表单
  • HistoryService:历史记录相关

  • ManagementService:它允许检索关于数据库表和表元数据的信息。此外,它还公开了作业的查询功能和管理操作。作业在引擎中用于各种用途,如计时器、异步延续、延迟挂起/激活等

  • FilterService:允许创建和管理过滤器。过滤器是像任务查询一样存储的查询。Tasklist(自带的页面)使用过滤器来过滤用户任务。

    • Filter相关说明
  • ExternalTaskService:提供对外部任务实例的访问。(见第一部分)

  • CaseService:同RunService,不过用于案例

  • DecisionService:决策相关服务

3.5 用户业务

Modeler可参考

前置工作:使用平台自行创建一个员工

我们接下来创建一个请假流程:员工请假,上级审批。


创建如下审批流程:
在这里插入图片描述
初始化变量 starter,流程引擎将在流程发起时,自动将发起人信息填入。
在这里插入图片描述
指派给starter,即发起者,可以指派给人、组、用户组、以及设置有效期
在这里插入图片描述
在员工请假处添加一个基础表单
在这里插入图片描述

在领导审批处添加另一个用户(此处直接指派给我们的管理员也可以)

在这里插入图片描述

完成后部署即可。

package com.engine.controller;import jakarta.annotation.Resource;
import org.camunda.bpm.engine.IdentityService;
import org.camunda.bpm.engine.RuntimeService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;@RestController
@RequestMapping("/leave")
public class LeaveController {/*** 用户、组、租户等相关* */@Resourceprivate IdentityService identityService;/*** 流程实例操作* */@Resourceprivate RuntimeService runtimeService;@GetMapping("/start/{user}")public void startProcess(@PathVariable("user")String user){/*** 表:act_id_user** 在此处我们设置发起人信息,对象存储在了如下类型中* ThreadLocal<Authentication> currentAuthentication = new ThreadLocal();* 因此线程之间是隔离的,不会造成混用情况* */identityService.setAuthenticatedUserId(user);/*** 表:* act_re_procdef BPMN定义* act_re_deployment流程部署信息* act_ge_bytearray 部署的实际内容* 开启一个流程实例** 开启后的实例存放于* 表:act_run_execution 以及  act_run_task* act_run_execution BPMN运行时记录,包含到那个节点了* act_run_task 流程总的记录,记录了执行到哪个节点了,指派给了谁等信息* 多个版本,默认使用最新版本* 启动加入流程变量时,需要加入startProcessInstanceByKey中* */runtimeService.startProcessInstanceByKey("Process_0pzlxi8");}
}

启动后调用接口,可以看到确实有一个流程,并且参数已经有了
在这里插入图片描述

我们使用页面去处理后,完成该流程。

3.6 流程启动Controller

可以做一个简单的启动类,帮助我们启动

package com.engine.controller;import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.camunda.bpm.engine.IdentityService;
import org.camunda.bpm.engine.RuntimeService;
import org.camunda.bpm.engine.runtime.ProcessInstance;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;@RestController
@Slf4j
@RequestMapping("/process")
public class ProcessController {@Resourceprivate IdentityService identityService;@Resourceprivate RuntimeService runtimeService;@GetMapping(value = {"" +"/start/{processKey}/user/{user}","/start/{processKey}","/start/{processKey}/businessKey/{businessKey}","/start/{processKey}/{businessKey}/{user}"})public void startProcess(@PathVariable("processKey") String processKey,@PathVariable(value = "user",required = false)String user,@PathVariable(value = "businessKey",required = false)String businessKey){if(user != null)identityService.setAuthenticatedUserId(user);ProcessInstance processInstance = null;if(businessKey != null)processInstance = runtimeService.startProcessInstanceByKey(processKey,businessKey);else processInstance =runtimeService.startProcessInstanceByKey(processKey);log.info("启动成功,\nInstanceId:{}\nDefinitionId:{}\nInstanceId:{}\nstarter:{}",processInstance.getRootProcessInstanceId(),processInstance.getProcessDefinitionId(),processInstance.getProcessInstanceId(),identityService.getCurrentAuthentication() == null?null:identityService.getCurrentAuthentication().getUserId());}
}

3.7 业务任务-内部任务

创建如下结构

在这里插入图片描述

3.7.1 Java Class实现

在预约修理家电实现处选择java class,表示以流程引擎自己的Java服务实现。随后我们在集成的服务里面新建一个ReserveRepairService 服务,并把路径添加进去。

在这里插入图片描述

package com.engine.serviceTask;import lombok.extern.slf4j.Slf4j;
import org.camunda.bpm.engine.delegate.DelegateExecution;
import org.camunda.bpm.engine.delegate.JavaDelegate;@Slf4j
public class ReserveRepairService implements JavaDelegate {@Overridepublic void execute(DelegateExecution execution) throws Exception {log.info("\n当前流程实例-{}\n执行-{}\n事件名称-{}",execution.getProcessInstanceId(),execution.getCurrentActivityName(),execution.getEventName());}
}

3.7.2 Delegate Expression实现

在这里插入图片描述
使用Delegate表达式,可以让流程引擎直接调用自己的Bean,我们需要让这个bean实现Camunda提供的包,默认调用execute。

package com.engine.serviceTask;import lombok.extern.slf4j.Slf4j;
import org.camunda.bpm.engine.delegate.DelegateExecution;
import org.camunda.bpm.engine.delegate.JavaDelegate;
import org.springframework.stereotype.Service;@Service("doRepair")
@Slf4j
public class DoRepairService implements JavaDelegate {@Overridepublic void execute(DelegateExecution execution) throws Exception {log.info("\n当前流程实例-{}\n执行-{}\n事件名称-{}",execution.getProcessInstanceId(),execution.getCurrentActivityName(),execution.getEventName());execution.setVariable("repairManName","打工人A");}
}

3.7.3 Express实现

在这里插入图片描述
该方式让流程引擎调用指定Bean的方法。需要指定接收返回的变量

package com.engine.serviceTask;import lombok.extern.slf4j.Slf4j;
import org.camunda.bpm.engine.delegate.DelegateExecution;
import org.springframework.stereotype.Service;@Service("telCall")
@Slf4j
public class TelCallService {public Integer doCall(DelegateExecution execution){log.info("\n当前流程实例-{}\n执行-{}\n事件名称-{}",execution.getProcessInstanceId(),execution.getCurrentActivityName(),execution.getEventName());/*** VariablesLocal 是该活动的变量,生命周期与活动一致。* Variables 是流程变量,生命周期与流程一致。当设置同名变量时,会进行覆盖。* 所有变量会:* 保存在表 act_ru_variable中,当有Task_ID时,代表是Local* act_hi_varinst 为历史变量表* */String repairManName = String.valueOf(execution.getVariable("repairManName"));log.info("请对-{}的服务打分",repairManName);return 10;}
}

我们也可以用#号表达式
在这里插入图片描述

 	public void getScore(DelegateExecution execution){log.info("\n当前流程实例-{}\n执行-{}\n事件名称-{}",execution.getProcessInstanceId(),execution.getCurrentActivityName(),execution.getEventName());log.info("员工-{}的服务得分是:{}",execution.getVariable("repairManName"),execution.getVariable("score"));}

执行结果:
在这里插入图片描述

可以发现,这个我们启动的log在最后才打出来,是同步执行的。

3.7.4 流程的回退与重启与暂停

回退

可以参考Process Instance Modification

继续上述逻辑,我们想要在回访后,重新设置评分。

	@ResourceRuntimeService runtimeService;@ResourceRepositoryService repositoryService;public Integer doCall(DelegateExecution execution){log.info("\n当前流程实例-{}\n执行-{}\n事件名称-{}",execution.getProcessInstanceId(),execution.getCurrentActivityName(),execution.getEventName());if(execution.getVariable("redo") != null && (Boolean) execution.getVariable("redo") ){log.info("重新打分:6分");execution.setVariable("rescore",6);return (Integer) execution.getVariable("score");}String repairManName = String.valueOf(execution.getVariable("repairManName"));log.info("请对-{}的服务打分",repairManName);return 10;}public void getScore(DelegateExecution execution){log.info("\n当前流程实例-{}\n执行-{}\n事件名称-{}",execution.getProcessInstanceId(),execution.getCurrentActivityName(),execution.getEventName());if(execution.getVariable("rescore") == null){ProcessDefinitionEntity processDefinitionEntity = (ProcessDefinitionEntity)repositoryService.getProcessDefinition(execution.getProcessDefinitionId());List<ActivityImpl> activityList = processDefinitionEntity.getActivities();log.info("该流程的所有活动\n{}\n",activityList);int target = -1;int i = 0;for(;i < activityList.size(); i++,target++){if(activityList.get(i).getActivityId() == execution.getCurrentActivityId()){break;}}if(target < 0){target = 0;}log.info("\n当前节点位置:{},id:{},name:{}\n回退节点位置:{},id:{},name:{}",i,activityList.get(i).getActivityId(),activityList.get(i).getName(),target,activityList.get(target).getActivityId(),activityList.get(target).getName());log.info("想要重新打分");// 修改当前流程至上一流程前runtimeService.createProcessInstanceModification(execution.getProcessInstanceId()).startBeforeActivity(activityList.get(target).getActivityId(),execution.getActivityInstanceId()).setVariable("redo",true).cancelActivityInstance(execution.getActivityInstanceId()).execute();return;}log.info("员工-{}的服务得分是:{},修改得分是:{}",execution.getVariable("repairManName"),execution.getVariable("score"),execution.getVariable("rescore"));}

在这里插入图片描述

重启

依据官方例子流程重启动
在这里插入图片描述

直接删除实例

ProcessInstance processInstance = runtimeService.createProcessInstanceQuery().singleResult();
runtimeService.deleteProcessInstance(processInstance.getId(), "any reason");

然后重新启动即可

runtimeService.restartProcessInstance(processInstance.getProcessDefinitionId()).startBeforeActivity("receivePayment").startBeforeActivity("shipOrder").processInstanceIds(processInstance.getId()).execute();

重启后,全局变量将会带入重启流程。需手动设置局部变量,例如通过调用 。RuntimeService.setVariableLocal(…)

从技术上讲,已创建一个新的流程实例。历史进程实例和重新启动的进程实例的 ID 不同。

也可以使用-RESTAPI

暂停

使用如下方法即可

runtimeService.suspendProcessInstanceById();runtimeService.startProcessInstanceById();

3.8 业务任务-外部任务

可参考-外部任务

外部任务的执行流程:

4.complete()
2.fetchAndLock()
3.handle
1.subscribe(topic) & register(handler)
Custom(Task)Handler
External Task Client
Camunda Rest API
  1. 外部任务注册至External Task Client
  2. 有实例任务时,由External Task Client抓取任务并锁定(避免重复消费)
  3. 分发至对应外部任务
  4. 外部任务完成后告知External Task Client
  5. 失败时上报异常,流程会卡在该节点:可重试。

3.8.1 异步响应:长轮询(Long Polling)

流程引擎对于客户端设计为长轮询模式拉取任务。可参考长轮询的实现方式

  1. 当没有外部任务可以用的时候,请求会被服务器挂起并加锁,防止重复消费
  2. 一旦有新的外部任务可以执行时,就会重新激活请求并执行响应
  3. 设置超时时间,可以在超时后释放该锁并不在挂起该任务

3.8.2 注解方式

非注解方式见第一部分。
官方文档 spring-boot-starter外部任务配置项 7.20

配置与依赖

此处可以另起一个项目。
引入如下同版本包

		<?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>org.example</groupId><artifactId>CamudaDemo</artifactId><version>1.0-SNAPSHOT</version><properties><maven.compiler.source>17</maven.compiler.source><maven.compiler.target>17</maven.compiler.target></properties><dependencyManagement><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-dependencies</artifactId><version>3.1.1</version><type>pom</type><scope>import</scope></dependency></dependencies></dependencyManagement><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></dependency><dependency><groupId>org.camunda.bpm.springboot</groupId><artifactId>camunda-bpm-spring-boot-starter-external-task-client</artifactId><version>7.20.0</version></dependency></dependencies>
</project>

配置

server:port: 8081
camunda:bpm:client:# 流程引擎地址base-url: http://localhost:8080/engine-rest# 长轮询持续时间(异步响应超时时间),默认为null# 设置后开启长轮询async-response-timeout: 20000# 一次最多拉取任务数量,默认10max-tasks: 1# 订阅topic的上锁时间,超时后,其他外部任务才能获取# 优先级小余直接在接口上配置,默认20,000lock-duration: 10000# 当前工作节点的IDworker-id: camunda-demo
spring:application:name: camunda demo
写一个外部任务

沿用上述流程,添加如下配置:
在这里插入图片描述

package com.camunda.demo.camundaSubscribe;import lombok.extern.slf4j.Slf4j;
import org.camunda.bpm.client.spring.annotation.ExternalTaskSubscription;
import org.camunda.bpm.client.task.ExternalTaskHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;@Configuration
@Slf4j
public class RepairSubscribe {
//全局配置优先级小余私有配置,因此超时会以此处为准@Bean@ExternalTaskSubscription(topicName = "try_self_repair",processDefinitionKeyIn = {"Process_10qrmih"},lockDuration = 2000)public ExternalTaskHandler TrySelfRepair(){return ((externalTask, externalTaskService) -> {/*** 是否免费修* */Boolean isFree = true;if(isFree){log.info("免费维修");externalTaskService.complete(externalTask);}else{log.info("自己修");/***  message stacktrace 重试次数 重试超时时间* 重试次数为0时,会创建一个异常事件* */externalTaskService.handleFailure(externalTask,"message-自己修不好","stacktrace",0,5000);}});}
}

为了方便,我们手动调整true,false来看一下效果。
true时:
在这里插入图片描述
可以看到转为外部服务时,就不再是同步等待了。
在这里插入图片描述
false时:

可以看到创建了一个异常
在这里插入图片描述

重试方式

需要设置重试时可以用如下格式:

Integer retries = 3if (externaltask.getRetries() != null) {retries = externaltask.getRetries() - 1;
}
externaltaskService.failure("Reason", retries);

在这里插入图片描述

优先级

在一个流程中,各个节点可以设置优先级,通过优先级,来让哪个流程实先执行。

优先级越高越先执行,默认都为0

在这里插入图片描述

3.9 任务监听器(引擎端)

在引擎端使用监听器,

添加如下配置,在结束时判断用户是否添写上门修理的地址,没有则使用默认地址

在这里插入图片描述

同理,于上门修理处添加一个监听器,在开始时监测

在这里插入图片描述

package com.engine.listener;import io.micrometer.common.util.StringUtils;
import lombok.extern.slf4j.Slf4j;
import org.camunda.bpm.engine.delegate.DelegateExecution;
import org.camunda.bpm.engine.delegate.ExecutionListener;
import org.springframework.stereotype.Component;@Component("checkHomeAddress")
@Slf4j
public class CheckHomeAddressListener implements ExecutionListener {@Overridepublic void notify(DelegateExecution execution) throws Exception {log.info("校验用户");String homeAddress = (String) execution.getVariable("homeAddress");if(StringUtils.isBlank(homeAddress)){log.info("未获取到地址");execution.setVariable("homeAddress","默认地址");}}
}
package com.engine.listener;import lombok.extern.slf4j.Slf4j;
import org.camunda.bpm.engine.delegate.DelegateExecution;
import org.camunda.bpm.engine.delegate.ExecutionListener;
import org.springframework.stereotype.Component;@Component("noticeCustomer")
@Slf4j
public class NoticeCustomerListener implements ExecutionListener {@Overridepublic void notify(DelegateExecution execution) throws Exception {log.info("开始通知客户");String homeAddress = (String) execution.getVariable("homeAddress");log.info("尊敬的客户您好,维修师傅正在前往:{},请耐心等候",homeAddress);}
}

在这里插入图片描述

3.10 鉴权

官方可以用web.xml配置,建议使用另外实现。
比如整合SpringSecurity等。

3.11 多实例任务

当需要执行一个循环的流程时,可以让其作为多实例任务。
多实例任务分为:
- 串行多实例任务
- 并行多实例任务

修改前面的请假流程,让它变为多实例任务。

下图中,可以看到:

  • 三根竖线:异步执行
  • 三根横线:顺序执行

在这里插入图片描述

在这里插入图片描述
可以看到上图可以添加多实例

  • Loop cardinality:执行循环次数
  • Completion condition:循环跳出条件,当满足时,可以提前结束循环
  • collection:不指定循环次数,直接循环对象(list)
  • Element:List 中的E
  • Asynchronous before/after:异步前/异步后操作

3.11.1 顺序执行

我们先做顺序执行:
在这里插入图片描述
另外写一个启动类,并提前添加用户:test,zhangsan,lisi,wangwu

@GetMapping("/start/multi")public void startProcess(){identityService.setAuthenticatedUserId("test");List<String> leaders = new ArrayList<>();leaders.add("zhangsan");leaders.add("lisi");leaders.add("wangwu");VariableMap variableMap = Variables.createVariables();variableMap.put("leaders",leaders);ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("Process_multi_task",variableMap);log.info("启动成功,\nInstanceId:{}\nDefinitionId:{}\nInstanceId:{}\nstarter:{}",processInstance.getRootProcessInstanceId(),processInstance.getProcessDefinitionId(),processInstance.getProcessInstanceId(),identityService.getCurrentAuthentication() == null ? null : identityService.getCurrentAuthentication().getUserId());}

如果我们在使用张三前先用李四的账号,会发现虽然有但不能评价。

3.11.2 多实例内置变量

同init的时候后可以直接用内置变量starter,多任务中可以使用如下内置变量:

  • nrOfActiveInstances:当前活动的实例数量
  • loopCounter:循环计数器,办理人在列表中的索引
  • nrOfInstances:多实例任务中总共的实例数目
  • nrOfCompletedInstances:已经完成的实例数量

3.11.3 并行执行

如下图修改,只要有两个人同意就可以。

在这里插入图片描述

此时评价将不分先后,此外可以看到同时开始了循流程
在这里插入图片描述

3.12 脚本任务

Camunda支持大多是JSR-223的脚本引擎。比如JavaScript、Groovy等。

对前面请假流程增加扣年假这个操作。

innline 即在如下图中写入
在这里插入图片描述
External则是外部,通过路径引用,需要给出返回变量名。
需要用如下前缀:

classpath://  也就是需要放置于工程目录下,通过Springboot的逻辑加载
deployment:// 也就是需要放置于模型扫描目录下,一同部署于数据库

附录

camunda中文-官方文档
camunda内部构造
camunda英文-官方文档
blibli-camunda工作流实战课程
camunda数据库表结构介绍

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/223678.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

typedef的使用

在C语言中&#xff0c;有一个关键字叫做typedef&#xff0c;有些人对此感到很疑惑。不熟悉此知识的同学都会对编程失去细心&#xff0c;直接劝退&#xff08;因为之前我就是这样&#xff09;。、 因为好不容易认识了C语言中所有的关键字&#xff08;就是类型吧&#xff0c;像啥…

详细教程 - 从零开发 Vue 鸿蒙harmonyOS应用 第一节

关于使用Vue开发鸿蒙应用的教程,我这篇之前的博客还不够完整和详细。那么这次我会尝试写一个更加完整和逐步的指南,从环境准备,到目录结构,再到关键代码讲解,以及调试和发布等,希望可以让大家详实地掌握这个过程。 一、准备工作 下载安装 DevEco Studio 下载地址&#xff1a;…

逻辑回归的介绍和应用

逻辑回归的介绍 逻辑回归&#xff08;Logistic regression&#xff0c;简称LR&#xff09;虽然其中带有"回归"两个字&#xff0c;但逻辑回归其实是一个分类模型&#xff0c;并且广泛应用于各个领域之中。虽然现在深度学习相对于这些传统方法更为火热&#xff0c;但实…

基础算法(3):排序(3)插入排序

1.插入排序实现 插入排序的工作原理是&#xff1a;通过构建有序序列&#xff0c;对于未排序数据&#xff0c;在已经排序的序列从后向前扫描&#xff0c;找到位置并插入&#xff0c;类似于平时打扑克牌时&#xff0c;将牌从大到小排列&#xff0c;每次摸到一张牌就插入到正确的位…

12.4~12.14概率论复习与相应理解(学习、复习、备考概率论,这一篇就够了)

未分配的题目 概率计算&#xff08;一些转换公式与全概率公式&#xff09;与实际概率 &#xff0c;贝叶斯 一些转换公式 相关性质计算 常规&#xff0c;公式的COV与P 复习相关公式 计算出新表达式的均值&#xff0c;方差&#xff0c;再套正态分布的公式 COV的运算性质 如…

前后端项目,nginx部署前端项目后刷新浏览器报错404的问题

问题&#xff1a; Vue单页应用项目打包部署Nginx服务器后&#xff0c;刷新页面后&#xff0c;出现404。 原因&#xff1a; 加载单页应用后路由改变均由浏览器处理&#xff0c;而刷新时将会请求当前的链接&#xff0c;而Nginx无法找到对应的页面。 解决&#xff1a; 在Nginx配…

python爬虫学习-批量爬取图片

python爬虫学习-批量爬取图片 爬虫步骤爬取前十页图片到本地根据页码获取网络源码使用xpath解析网页解析网页并下载图片主函数如下 爬取的网站为站长素材&#xff08;仅做学习使用&#xff09; 爬取的目标网站为 https://sc.chinaz.com/tupian/qinglvtupian.html如果爬取多页&…

大数据讲课笔记1.2 Linux用户操作

文章目录 零、学习目标一、导入新课二、新课讲解&#xff08;一&#xff09;用户账号管理1、用户与用户组文件2、用户账号管理工作 &#xff08;二&#xff09;用户操作1、切换用户&#xff08;1&#xff09;语法格式&#xff08;2&#xff09;切换到普通用户&#xff08;3&…

NVH软件导入音频文件

我们经常会遇到一种情况是&#xff1a;车主上下班路上经常会听到一个异响&#xff0c;但车交到我们手上&#xff0c;我们怎么在外面去试车&#xff0c;都听不到这个异响&#xff0c;或者条件达不到重现不了这个异响。如果是这样&#xff0c;我们是不是有点崩溃&#xff1f;但&a…

jstree组件的使用详细教程,部分案例( PHP / fastAdmin )

jstree 组件的使用。 简介&#xff1a;JsTree是一个jquery的插件&#xff0c;它提交一个非常友好并且强大的交互性的树&#xff0c;并且是完全免费或开源的&#xff08;MIT 许可&#xff09;。Jstree技持Html 或 json格式的的数据&#xff0c; 或者是ajax方式的动态请求加载数…

公司团队规范研发流程概要

一、背景 ● 背景&#xff1a;XXX研发部门开发流程步骤以及开发工具&#xff0c;依赖版本&#xff0c;开发规范等相关信息。 ● 技术定位&#xff1a;All。 ● 目标群体&#xff1a;所有相关研发部门技术人员。 二、操作步骤 2.1 开发前的准备 准备工作一 开发相关账号开通…

中职网络安全应急响应—Server2228

应急响应 任务环境说明: 服务器场景:Server2228(开放链接) 用户名:root,密码:p@ssw0rd123 1. 找出被黑客修改的系统别名,并将倒数第二个别名作为Flag值提交; 通过用户名和密码登录系统 在 Linux 中,利用 “alias” 命令去查看当前系统中定义的所有别名 flag:ss …

软实力篇---第二篇

系列文章目录 文章目录 系列文章目录前言一、必知必会的几点二、必须了解的两大法则三、项目经历怎么写前言 前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站,这篇文章男女通用,看懂了就去分享给你的码吧。 一、必知必…

Echarts饼图中显示百分比

开发中遇到一个需求&#xff0c;要在饼图上显示数据百分比&#xff0c;下图&#xff1a; 查了echarts 文档&#xff0c;并不能通过简单的配置来实现&#xff0c;原因如下&#xff1a;在单个serie的label中&#xff0c;只能设置一个label&#xff0c;位置可以选择在饼图内部inne…

在线监控网址源码/ 网站监控工具源码/ 网站监控系统源码/定时任务/网站网址URL状态监控神器

源码介绍&#xff1a; 在线监控网址源码、 网站监控工具源码&#xff0c;它作为网站监控系统源码&#xff0c;有定时任务&#xff0c;支持卡密充值&#xff0c;是网站网址URL状态监控神器。让数据库监控更加简单和专业。远程云中监控、实时邮件告警、丰富的指标和图表、分析和…

【教学类-06-17】20231215 (题目填满55格)X-Y之间“加法题+题”

背景需求&#xff1a; 0-5加法、减法是大班孩子选择较多的题型&#xff0c;因为只有21题&#xff0c;做题速度快&#xff0c;完成后&#xff0c;&#xff0c;他们会问&#xff1a;“后面的空白格子做什么” “可以画画&#xff0c;自己出题目” 但是大部分孩子都不会自己出题目…

【导航栏内容的设置 Objective-C语言】

一、那接下来呢,我们就来做一做,关于导航控制器, 1.设置它顶部的导航栏儿内容的东西, 1)我们刚刚讲过的这个,通过代码去跳转、返回、 2)通过storyboard去跳转、返回、 但是,这两种情况,大家是不是已经注意到,导航栏里面,没有任何内容, 然后呢,返回,这是红色,…

C++ Qt开发:ProgressBar进度条组件

Qt 是一个跨平台C图形界面开发库&#xff0c;利用Qt可以快速开发跨平台窗体应用程序&#xff0c;在Qt中我们可以通过拖拽的方式将不同组件放到指定的位置&#xff0c;实现图形化开发极大的方便了开发效率&#xff0c;本章将重点介绍ProgressBar进度条组件的常用方法及灵活运用。…

骨传导耳机可以保护听力吗?一文读懂骨传导耳机和开放式耳机的区别!

由于骨传导耳机通过人体骨骼来传递声音&#xff0c;不经过耳道&#xff0c;不会损伤耳膜以及内毛细胞&#xff0c;所以使用骨传导耳机不仅不会损伤听力&#xff0c;还能在一定程度上起到保护听力的作用。 一、骨传导耳机和开放式耳机的区别是什么&#xff1f; 由于骨传导耳机…