文章目录
- 一、项目介绍
- 二、问题复现
- 三、排查过程
- 1、找到报错信息
- 2、找到报错的堆栈信息
- 3.找到错误代码
- 4.分析错误原因
- 4.1、首先要明白`SecurityManager`是什么?
- 4.2、定时器前的流程跟之前的流程不是同一个流程
- 👉定时任务的数据已经生成,那么该如何生成定时任务呢?
- 👉介绍一下`JobExecutor`
- 👉所以服务器上的`JobExecutor`的执行并不绑定到特定的服务器
- 5、解决方案
- 6、定时作业执行器不执行的问题
一、项目介绍
当前项目底层是开源的jeecg框架,公司根据此框架进行二次开发。但是框架本身集成的一些功能并没有去,比如:activiti、xxl-job等。
版本:
jeecg-boot v3.3.0
集成的activiti v5.22.0
请注意一下网址👉官网地址、JeecgBoot 开发文档、github地址、相关的jeecgFlow文档
二、问题复现
根据项目中的流程设计。画的部分流程图,如下显示:
节点流转:
节点1
:是部门审批节点。部门审批通过之前,设置延时(结束)时间
,也可以叫下次派发时间
。节点2
:定时器节点:延时到下次派发时间,后流转到3节点
上节点3
:是部门派发节点。
因业务状态需要根据节点的流转进行同步的修改,所以在此基础上,我给每个节点都增加了一个create
事件的TaskListener
任务监听(监听Class:DefectFlowNodeStartTaskListener
),在监听里面获取到节点信息,通过FeginClient
接口修改业务数据的状态!
本质上来说,这是一个基本的业务,没啥难得,但是实际开发中遇到了一个问题,那就是节点2定时器
到时间后,执行DefectFlowNodeStartTaskListener
报错了!
问题来了,关于DefectFlowNodeStartTaskListener
的日志不打印,断点也拦不住!根本判断,错误出到哪里!!!
三、排查过程
1、找到报错信息
最后详细了解activiti
的知识,最后发现了,关于activiti-timer-job
的信息,都存储到一个张表act_ru_job
里。
-- auto-generated definition
create table act_ru_job
(
ID_ VARCHAR(64) not null comment '主键'
primary key,
REV_ INT(10) null comment '乐观锁',
TYPE_ VARCHAR(255) not null comment '类型',
LOCK_EXP_TIME_ TIMESTAMP(23) null comment '锁定过期时间',
LOCK_OWNER_ VARCHAR(255) null comment '锁定节点',
EXCLUSIVE_ TINYINT(3) null comment '是否唯一',
EXECUTION_ID_ VARCHAR(64) null comment '执行',
PROCESS_INSTANCE_ID_ VARCHAR(64) null comment '流程实例',
PROC_DEF_ID_ VARCHAR(64) null comment '流程定义',
RETRIES_ INT(10) null comment '重试次数',
EXCEPTION_STACK_ID_ VARCHAR(64) null comment '异常堆栈',
EXCEPTION_MSG_ VARCHAR(4000) null comment '异常信息',
DUEDATE_ TIMESTAMP(23) null comment '截止时间',
REPEAT_ VARCHAR(255) null comment '重复',
HANDLER_TYPE_ VARCHAR(255) null comment '处理器类型',
HANDLER_CFG_ VARCHAR(4000) null comment '处理器配置',
TENANT_ID_ VARCHAR(255) default '' null comment '租户id',
constraint ACT_FK_JOB_EXCEPTION
foreign key (EXCEPTION_STACK_ID_) references act_ge_bytearray (ID_)
);
最后在字段EXCEPTION_MSG_
里找到了报错信息。但是只有报错信息,没有堆栈信息,无法排查是哪一行的错误。
报错信息如下:
Exception while invoking TaskListener: Exception while invoking TaskListener: No SecurityManager accessible to the calling code, either bound to the org.apache.shiro.util.ThreadContext or as a vm static singleton. This is an invalid application config...
找到了一天半!都没解决!!
就想着找到报错的堆栈信息,包括不限于:打断点、通过错误日志找报错的那一行代码,通过代码分析报错的点、通过JVM
的jconsole
查找堆栈信息。
2、找到报错的堆栈信息
最后在头昏脑胀的时候,突然发现,上面那一张表,有一个字段叫EXCEPTION_STACK_ID_
,这个 异常堆栈
是一个ID
,而且还是一个外键。
这就有意思了,通过的IDEA DataGrid
的Show Diagram...
找到了对应表act_ge_bytearray
。
act_ge_bytearray
这张表比较有意思。
-- auto-generated definition
create table act_ge_bytearray
(
ID_ VARCHAR(64) not null comment '主键'
primary key,
REV_ INT(10) null comment '乐观锁',
NAME_ VARCHAR(255) null comment '部署的文件名称',
DEPLOYMENT_ID_ VARCHAR(64) null comment '部署表ID',
BYTES_ LONGBLOB(max) null comment '部署文件',
GENERATED_ TINYINT(3) null comment '是否是引擎生成(0为用户生成,1为activiti生成)',
constraint ACT_FK_BYTEARR_DEPL
foreign key (DEPLOYMENT_ID_) references act_re_deployment (ID_)
);
刚开始出现报错的时候,我曾猜测,可能是我流程图画错了,通过跟踪接口,最后发现了这张表。关于生成的activiti
的bpm.xml
、流程图生成的图片信息
、流程页面画的图的信息
等信息都存在入了这样表!
用官方的话说就是:ACT_GE_BYTEARRAY
表用于存储与流程定义相关的二进制数据,比如流程图的图片文件、模型的XML定义以及其他与流程实例或任务相关的文件。
本表字段
NAME_
:
字段值是stacktrace
都是执行出错的堆栈信息
字段值是bpmn20.xml
结尾的,是流程生成的bpm.xml
信息
字段值是source
的,是流程生成的bpm.xml
的json信息,比如:节点、连线关系。
字段值是source-extra
的,是流程定义的额外源代码信息。具体来说,这是指在BPMN模型中可能存在的额外元数据或者是在编辑流程模型时添加的附加信息。例如,如果你使用Activiti Modeler或其他支持的BPMN编辑器,那么source-extra可能包含编辑器特定的数据,如用户界面布局信息、注释、自定义属性等,这些数据不直接参与流程执行,但在编辑或展示流程模型时有用。
当时因为数据太多,根本没发现里面存在堆栈信息。
最后根据上面的EXCEPTION_STACK_ID_
关联到本表的ID_
上,才查询出来报错的堆栈信息。
org.activiti.engine.ActivitiException: Exception while invoking TaskListener: Exception while invoking TaskListener: No SecurityManager accessible to the calling code, either bound to the org.apache.shiro.util.ThreadContext or as a vm static singleton. This is an invalid application configuration.
at org.activiti.engine.impl.persistence.entity.TaskEntity.fireEvent(TaskEntity.java:742)
at org.activiti.engine.impl.bpmn.behavior.UserTaskActivityBehavior.execute(UserTaskActivityBehavior.java:212)
at org.activiti.engine.impl.pvm.runtime.AtomicOperationActivityExecute.execute(AtomicOperationActivityExecute.java:60)
at org.activiti.engine.impl.interceptor.CommandContext.performOperation(CommandContext.java:97)
at org.activiti.engine.impl.persistence.entity.ExecutionEntity.performOperationSync(ExecutionEntity.java:650)
at org.activiti.engine.impl.persistence.entity.ExecutionEntity.performOperation(ExecutionEntity.java:643)
at org.activiti.engine.impl.pvm.runtime.AtomicOperationTransitionNotifyListenerStart.eventNotificationsCompleted(AtomicOperationTransitionNotifyListenerStart.java:52)
at org.activiti.engine.impl.pvm.runtime.AbstractEventAtomicOperation.execute(AbstractEventAtomicOperation.java:56)
at org.activiti.engine.impl.interceptor.CommandContext.performOperation(CommandContext.java:97)
at org.activiti.engine.impl.persistence.entity.ExecutionEntity.performOperationSync(ExecutionEntity.java:650)
at org.activiti.engine.impl.persistence.entity.ExecutionEntity.performOperation(ExecutionEntity.java:643)
at org.activiti.engine.impl.pvm.runtime.AbstractEventAtomicOperation.execute(AbstractEventAtomicOperation.java:49)
at org.activiti.engine.impl.interceptor.CommandContext.performOperation(CommandContext.java:97)
at org.activiti.engine.impl.persistence.entity.ExecutionEntity.performOperationSync(ExecutionEntity.java:650)
at org.activiti.engine.impl.persistence.entity.ExecutionEntity.performOperation(ExecutionEntity.java:643)
at org.activiti.engine.impl.pvm.runtime.AtomicOperationTransitionCreateScope.execute(AtomicOperationTransitionCreateScope.java:49)
at org.activiti.engine.impl.interceptor.CommandContext.performOperation(CommandContext.java:97)
at org.activiti.engine.impl.persistence.entity.ExecutionEntity.performOperationSync(ExecutionEntity.java:650)
at org.activiti.engine.impl.persistence.entity.ExecutionEntity.performOperation(ExecutionEntity.java:643)
at org.activiti.engine.impl.pvm.runtime.AtomicOperationTransitionNotifyListenerTake.execute(AtomicOperationTransitionNotifyListenerTake.java:80)
at org.activiti.engine.impl.interceptor.CommandContext.performOperation(CommandContext.java:97)
at org.activiti.engine.impl.persistence.entity.ExecutionEntity.performOperationSync(ExecutionEntity.java:650)
at org.activiti.engine.impl.persistence.entity.ExecutionEntity.performOperation(ExecutionEntity.java:643)
at org.activiti.engine.impl.pvm.runtime.AtomicOperationTransitionDestroyScope.execute(AtomicOperationTransitionDestroyScope.java:116)
at org.activiti.engine.impl.interceptor.CommandContext.performOperation(CommandContext.java:97)
at org.activiti.engine.impl.persistence.entity.ExecutionEntity.performOperationSync(ExecutionEntity.java:650)
at org.activiti.engine.impl.persistence.entity.ExecutionEntity.performOperation(ExecutionEntity.java:643)
at org.activiti.engine.impl.pvm.runtime.AtomicOperationTransitionNotifyListenerEnd.eventNotificationsCompleted(AtomicOperationTransitionNotifyListenerEnd.java:35)
at org.activiti.engine.impl.pvm.runtime.AbstractEventAtomicOperation.execute(AbstractEventAtomicOperation.java:56)
at org.activiti.engine.impl.interceptor.CommandContext.performOperation(CommandContext.java:97)
at org.activiti.engine.impl.persistence.entity.ExecutionEntity.performOperationSync(ExecutionEntity.java:650)
at org.activiti.engine.impl.persistence.entity.ExecutionEntity.performOperation(ExecutionEntity.java:643)
at org.activiti.engine.impl.pvm.runtime.AbstractEventAtomicOperation.execute(AbstractEventAtomicOperation.java:49)
at org.activiti.engine.impl.interceptor.CommandContext.performOperation(CommandContext.java:97)
at org.activiti.engine.impl.persistence.entity.ExecutionEntity.performOperationSync(ExecutionEntity.java:650)
at org.activiti.engine.impl.persistence.entity.ExecutionEntity.performOperation(ExecutionEntity.java:643)
at org.activiti.engine.impl.persistence.entity.ExecutionEntity.take(ExecutionEntity.java:460)
at org.activiti.engine.impl.persistence.entity.ExecutionEntity.take(ExecutionEntity.java:438)
at org.activiti.engine.impl.bpmn.behavior.BpmnActivityBehavior.performOutgoingBehavior(BpmnActivityBehavior.java:140)
at org.activiti.engine.impl.bpmn.behavior.BpmnActivityBehavior.performDefaultOutgoingBehavior(BpmnActivityBehavior.java:66)
at org.activiti.engine.impl.bpmn.behavior.FlowNodeActivityBehavior.leave(FlowNodeActivityBehavior.java:44)
at org.activiti.engine.impl.bpmn.behavior.AbstractBpmnActivityBehavior.leave(AbstractBpmnActivityBehavior.java:47)
at org.activiti.engine.impl.bpmn.behavior.IntermediateCatchEventActivityBehavior.signal(IntermediateCatchEventActivityBehavior.java:27)
at org.activiti.engine.impl.persistence.entity.ExecutionEntity.signal(ExecutionEntity.java:417)
at org.activiti.engine.impl.jobexecutor.TimerCatchIntermediateEventJobHandler.execute(TimerCatchIntermediateEventJobHandler.java:58)
at org.activiti.engine.impl.persistence.entity.JobEntity.execute(JobEntity.java:85)
at org.activiti.engine.impl.persistence.entity.TimerEntity.execute(TimerEntity.java:96)
at org.activiti.engine.impl.cmd.ExecuteJobsCmd.execute(ExecuteJobsCmd.java:88)
at org.activiti.engine.impl.interceptor.CommandInvoker.execute(CommandInvoker.java:24)
at org.activiti.engine.impl.interceptor.CommandContextInterceptor.execute(CommandContextInterceptor.java:57)
at org.activiti.spring.SpringTransactionInterceptor$1.doInTransaction(SpringTransactionInterceptor.java:47)
at org.springframework.transaction.support.TransactionTemplate.execute(TransactionTemplate.java:140)
at org.activiti.spring.SpringTransactionInterceptor.execute(SpringTransactionInterceptor.java:45)
at org.activiti.engine.impl.interceptor.LogInterceptor.execute(LogInterceptor.java:31)
at org.activiti.engine.impl.cfg.CommandExecutorImpl.execute(CommandExecutorImpl.java:40)
at org.activiti.engine.impl.cfg.CommandExecutorImpl.execute(CommandExecutorImpl.java:35)
at org.activiti.engine.impl.jobexecutor.ExecuteJobsRunnable.handleMultipleJobs(ExecuteJobsRunnable.java:94)
at org.activiti.engine.impl.jobexecutor.ExecuteJobsRunnable.run(ExecuteJobsRunnable.java:49)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)
Caused by: org.activiti.engine.ActivitiException: Exception while invoking TaskListener: No SecurityManager accessible to the calling code, either bound to the org.apache.shiro.util.ThreadContext or as a vm static singleton. This is an invalid application configuration.
at org.activiti.engine.impl.bpmn.helper.ClassDelegate.notify(ClassDelegate.java:123)
at org.activiti.engine.impl.delegate.TaskListenerInvocation.invoke(TaskListenerInvocation.java:34)
at org.activiti.engine.impl.delegate.DelegateInvocation.proceed(DelegateInvocation.java:37)
at org.activiti.engine.impl.delegate.DefaultDelegateInterceptor.handleInvocation(DefaultDelegateInterceptor.java:25)
at org.activiti.engine.impl.persistence.entity.TaskEntity.fireEvent(TaskEntity.java:740)
... 60 more
Caused by: org.apache.shiro.UnavailableSecurityManagerException: No SecurityManager accessible to the calling code, either bound to the org.apache.shiro.util.ThreadContext or as a vm static singleton. This is an invalid application configuration.
at org.apache.shiro.SecurityUtils.getSecurityManager(SecurityUtils.java:123)
at org.apache.shiro.subject.Subject$Builder.<init>(Subject.java:626)
at org.apache.shiro.SecurityUtils.getSubject(SecurityUtils.java:56)
at org.jeecg.modules.activiti.tasklistener.defetct.DefectFlowNodeStartTaskListener.notify(DefectFlowNodeStartTaskListener.java:42)
at org.activiti.engine.impl.delegate.TaskListenerInvocation.invoke(TaskListenerInvocation.java:34)
at org.activiti.engine.impl.delegate.DelegateInvocation.proceed(DelegateInvocation.java:37)
at org.activiti.engine.impl.delegate.DefaultDelegateInterceptor.handleInvocation(DefaultDelegateInterceptor.java:25)
at org.activiti.engine.impl.bpmn.helper.ClassDelegate.notify(ClassDelegate.java:121)
... 64 more
3.找到错误代码
最后根据报错的堆栈信息
找到了一行代码:
LoginUser loginUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();
4.分析错误原因
为什么获取LoginUser
的时候会报错SecurityManager
的错误呢?
4.1、首先要明白SecurityManager
是什么?
SecurityManager
是 Java 安全模型的核心组件之一,它提供了一种机制来控制程序中的敏感操作,如文件和网络访问、系统属性读取、类加载以及反射操作等。SecurityManager
允许应用程序开发者定义安全策略,从而决定在运行时哪些操作是被允许的,哪些是被禁止的。
主要功能
- 安全策略管理:
- 定义安全策略,规定哪些操作可以在程序中执行。
- 权限控制:
- 控制对敏感资源的访问权限,比如文件系统、网络连接、系统属性等。
- 执行检查:
- 包含一系列以 check 开头的方法,用于在执行潜在敏感操作前进行权限检查。
例如,checkRead(String file) 方法会在读取文件前检查权限。- 异常抛出:
- 如果 SecurityManager 认定某个操作不被允许,它会通过抛出 SecurityException 或其子类来阻止该操作的执行。
- 默认实例:
- 在 JVM 启动时,默认的 SecurityManager 实例可能不会启用,除非显式地设置了 -Djava.security.manager 参数或在安全策略文件中指定了它。
- Policy 文件:
- 通常,SecurityManager 使用一个名为 policy 的配置文件来确定哪些代码具有什么权限。
- 这个文件可以指定代码来源(代码基)与相应的权限集之间的映射。
这个类,咱们最常用的就是获取用户信息,就上面那个一段代码:
LoginUser loginUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();
跟踪SecurityUtils.getSubject()
方法,就会发现ThreadContext
类
public static Subject getSubject() {
Subject subject = ThreadContext.getSubject();
if (subject == null) {
subject = (new Subject.Builder()).buildSubject();
ThreadContext.bind(subject);
}
return subject;
}
跟踪ThreadContext.getSubject()
方法,进入ThreadContext
类就会发现一个属性
private static final ThreadLocal<Map<Object, Object>> resources = new InheritableThreadLocalMap<Map<Object, Object>>();
咱们的Subject
就是从这个属性resources
里获取的。看到ThreadLocal
大家应该就明白了。报错估计就是从这里来的。
定时器到时候后,执行taskListener
,结果监听里面有一个获取用户的方法!结果去ThreadLocal
里获取用户,获取不到用户信息就报错了!
为什么获取不到用户?之前的流程跟定时器开启的流程,不是同一个sessionId
的实例。
注意:
Jeecg-boot
框架集成了shiro
,上面的代码是Apache Shiro
的代码
4.2、定时器前的流程跟之前的流程不是同一个流程
这就要说到activiti
定时器了,他是怎么执行的,他是如何发起新的流程的!
先说,关于activiti
的定时器的数据。当上一节点【部门审批】审批通过后,就会生成关于定时器的任务定时数据,存入到表act_ru_job
中。这个上面已说,详细的不在赘述!
数据已经存储成功,那么接下来,就是定时任务的执行了!
👉定时任务的数据已经生成,那么该如何生成定时任务呢?
这时的定时器作业是由配置的JobExecutor(作业执行器)
来负责。
JobExecutor
的工作机制是定期查询数据库中的定时器作业,并执行那些达到触发时间的作业。
👉介绍一下JobExecutor
JobExecutor
是一个核心组件,用于异步执行各种类型的后台作业,包括但不限于定时器事件、信号事件、消息事件等。
1、JobExecutor
的主要职责
- 作业调度:
JobExecutor
负责定期查询数据库,找出那些需要立即执行的作业。它会检查作业的触发时间,并执行那些已经到达或超过触发时间的作业。 - 作业执行:一旦找到需要执行的作业,
JobExecutor
会将其从数据库中取出,并交给相应的JobHandler
进行处理。JobHandler
负责具体的作业处理逻辑,例如触发定时器事件、发送信号或消息等。 - 错误处理与重试:如果作业执行失败,
JobExecutor
会根据配置的策略进行重试。它可以配置重试次数和重试间隔,以确保即使在暂时的失败情况下,作业也能最终得到执行。 - 集群支持:在集群环境中,
JobExecutor
可以配置为分布式模式,允许多个服务器节点共享作业的执行。这提高了系统的可伸缩性和容错能力,确保即使部分节点出现故障,作业也能在其他节点上得到执行。
2、JobExecutor
的配置
JobExecutor
的配置通常在Activiti
的ProcessEngineConfiguration
中进行。以下是一些关键的配置选项:
asyncExecutorActivate
:启用或禁用JobExecutor的异步执行功能。asyncExecutor
:指定JobExecutor使用的异步执行器,例如线程池。lockTimeInMillis
:作业的锁定时间,以毫秒为单位。这决定了作业在被处理期间的锁定持续时间,防止其他JobExecutor实例尝试同时处理同一个作业。asyncJobAcquireWaitTimeInMillis
:JobExecutor在每次尝试获取作业之间的等待时间,以毫秒为单位。asyncJobLockTimeInMillis
:作业被锁定的时间长度,以毫秒为单位,这有助于防止作业被重复处理。
3、JobExecutor
的实现
Activiti
提供了几种不同的JobExecutor
实现,包括:
ThreadPoolJobExecutor
:使用线程池来执行作业,适合单个服务器或小型集群环境。StandaloneAsyncJobExecutor
:用于独立运行的环境,不建议在集群中使用。MuleJobExecutor
:专为Mule ESB
环境设计的JobExecutor
实现。
👉所以服务器上的JobExecutor
的执行并不绑定到特定的服务器
在单个服务器的环境中,JobExecutor
将直接在该服务器上执行定时器作业。但在集群环境中,多个服务器上的JobExecutor
实例会竞争执行数据库中的定时器作业,通常第一个获取到作业的JobExecutor
将锁定该作业并执行它。
因此,如果在集群中的服务器A
上启动的流程执行到了含有定时器的节点,定时器作业将被存储在数据库中。
当定时器触发时间到达时,任何一个集群中的JobExecutor
(包括服务器A
上的JobExecutor
)都有可能执行这个定时器作业,具体取决于哪个JobExecutor
首先获取并锁定了这个作业。
😭😭😭😭😭😭
其实,当我找到代码的错误后,我就已经修改成功了,但是后来,无论我怎么测试,数据库的act_ge_bytearray
表里存储的堆栈信息,永远都是这个。
而且有时候,还报其他的错误!日志不打印,断点拦不到。。。
😭😭😭因为定时器根本就没在我这执行!!!!😭😭😭
😭😭😭为了这个解决这个问题,我前前后后刷了两天半的时间,来排查这个问题!😭😭😭
😭😭😭本来不宽裕的开发时间吗,就更雪上加霜了!😭😭😭
这也就说明了,定时器前后,根本不是一个流程!在流程数据上说,可能是同一个processDefinitionId
的流程。
但是在JAVA看来,这根本就不是同一个实例!
毕竟生成定时器数据是一个服务器,执行定时任务的有可能是另一个服务器!
5、解决方案
第一个方案:其实不是为了解决最终的问题,而是避开这个错误,那就是不在【部门派发】节点增加create
事件的taskListener
。由【部门审批】节点delete
事件的taskListener
来做更新业务状态的操作。
但是这样的话,就需要自己写一个timer
,手动的定时更新业务数据的状态。
第二种方案:就是去掉那一行代码。。。但是么,这也有问题啊。你要让小伙伴们,合并你的代码。。。😂😂😂求人还是挺难的!!!好难开口啊!!!万一,你还没改对,还要让所有的小伙伴们,再合并一遍?😂😂😂
6、定时作业执行器不执行的问题
还有如果你是定时任务不执行的问题。那可能是你定时任务执行器就没开!!!添加如下配置:
spring:
activiti:
async-executor-activate: true
async-executor-enabled: true
最后提个醒:
子流程不能嵌入开始事件。
当嵌入定时开始事件的流程部署新版本,上一个版本的定时器作业会被移除。
一旦部署了流程,计时器启动事件就会被调度。没有必要调用startProcessInstanceBy
…,尽管调用start
进程方法是不受限制的,>并且会导致在startProcessInstanceBy
的时候再次启动进程…调用。