add - 为常用的接口添加防重提交限制功能

This commit is contained in:
wangli 2023-12-13 00:45:37 +08:00
parent 155e76af47
commit 7a369a42a9
20 changed files with 1188 additions and 181 deletions

View File

@ -22,6 +22,10 @@ public enum BpmnErrorCode implements IProjectRespCode {
CONVERTOR_META_DATA_FORMAT_ERROR("02002", "JSON 数据格式有误-{}: {}"),
CONVERTOR_COMMON_ERROR("02003", "JSON 转 BPMN 失败, 原因:【{}】"),
CONVERTOR_NODE_TYPE_NOT_SUPPORT("02004", "【{}】节点类型, ID:【{}】暂不支持"),
CONVERTOR_OPERATION_STRING_TYPE_ERROR("02005", "条件节点运算符【{}】暂不支持"),
CONVERTOR_OPERATION_NUMBER_TYPE_ERROR("02006", "条件节点运算符【{}】暂不支持"),
CONVERTOR_OPERATION_RADIO_TYPE_ERROR("02007", "条件节点运算符【{}】暂不支持"),
CONVERTOR_OPERATION_CHECKBOX_TYPE_ERROR("02008", "条件节点运算符【{}】暂不支持"),
// ========== bpmn model 03-001 ==========
@ -53,6 +57,8 @@ public enum BpmnErrorCode implements IProjectRespCode {
TASK_COMPLETE_FAIL_ASSIGN_NOT_SELF("06002", "该任务的审批人不是你"),
TASK_APOSTILLE_NOT_SUPPORT("06003", "当前加签模式不支持"),
TASK_REMIND_ERROR_NOT_EXISTS("06004", "当前审批节点没有待审批任务, 不能催办"),
ACTIVITY_TRIGGER_NOT_EXISTS("06005", "触发 ID:【{}】不存在"),
CALC_TASK_ASSIGNEE_ERROR("06006", "执行计算审批候选人出现异常: {}"),
// ========== form Model 07-001 ==========
FORM_MODEL_NOT_EXISTS("07001", "表单模型不存在"),
@ -64,6 +70,13 @@ public enum BpmnErrorCode implements IProjectRespCode {
// ========== flowable Engine 10-001 ==========
ENGINE_EXECUTION_LOST_ID_ERROR("10001", "Execution 丢失"),
ENGINE_USER_TASK_CALC_ERROR("10002", "计算用户任务节点的审批发生异常: 【{}】"),
ENGINE_USER_TASK_TYPE_NOT_SUPPORT("10003", "审批指定方式暂不支持"),
// ========== flowable Engine 99-001 ==========
MES_PUSH_OBJECT_BUILD_ERROR("99001", "构建消息推送对象异常"),
REPEAT_SUBMIT_TIME_ERROR_TIPS("99002", "重复提交间隔时间不能小于{}秒"),
REPEAT_SUBMIT_ERROR_TIPS("99002", "{}"),
// // ========== 流程模型 01-001 ==========

View File

@ -19,10 +19,6 @@ public class WorkflowEngineException extends ServiceException {
this.code = code.getRespCode();
}
public WorkflowEngineException(String message) {
super(message);
}
@Override
public String getCode() {
return this.code;

View File

@ -7,6 +7,11 @@ import com.google.common.collect.Lists;
import java.util.List;
import java.util.Objects;
import static cn.axzo.workflow.core.common.enums.BpmnErrorCode.CONVERTOR_OPERATION_CHECKBOX_TYPE_ERROR;
import static cn.axzo.workflow.core.common.enums.BpmnErrorCode.CONVERTOR_OPERATION_NUMBER_TYPE_ERROR;
import static cn.axzo.workflow.core.common.enums.BpmnErrorCode.CONVERTOR_OPERATION_RADIO_TYPE_ERROR;
import static cn.axzo.workflow.core.common.enums.BpmnErrorCode.CONVERTOR_OPERATION_STRING_TYPE_ERROR;
/**
* 表达式翻译器
*
@ -34,7 +39,7 @@ public final class BpmnExpressionTranslator {
.append("')");
} else {
// 其他非法的操作符都过滤掉,或在这里抛出异常
throw new WorkflowEngineException("非法的操作符");
throw new WorkflowEngineException(CONVERTOR_OPERATION_STRING_TYPE_ERROR, condition.getOperator());
}
return sb.toString();
}
@ -71,7 +76,7 @@ public final class BpmnExpressionTranslator {
}
return sb.toString();
} else {
throw new WorkflowEngineException("非法的操作符");
throw new WorkflowEngineException(CONVERTOR_OPERATION_NUMBER_TYPE_ERROR, condition.getOperator());
}
}
@ -85,7 +90,7 @@ public final class BpmnExpressionTranslator {
condition.getDefaultValue() +
")";
} else {
throw new WorkflowEngineException("非法的操作符");
throw new WorkflowEngineException(CONVERTOR_OPERATION_RADIO_TYPE_ERROR, condition.getOperator());
}
}
@ -102,7 +107,7 @@ public final class BpmnExpressionTranslator {
sb.append(")");
return sb.toString();
} else {
throw new WorkflowEngineException("非法的操作符");
throw new WorkflowEngineException(CONVERTOR_OPERATION_CHECKBOX_TYPE_ERROR, condition.getOperator());
}
}

View File

@ -12,6 +12,7 @@ import org.flowable.bpmn.model.UserTask;
import static cn.axzo.workflow.common.constant.BpmnConstants.CONFIG_NODE_TYPE;
import static cn.axzo.workflow.common.constant.BpmnConstants.FLOW_NODE_JSON;
import static cn.axzo.workflow.core.common.enums.BpmnErrorCode.CONVERTOR_NODE_TYPE_NOT_SUPPORT;
/**
* 抽象的 JSON BPMN 协议的转换器
@ -22,7 +23,7 @@ import static cn.axzo.workflow.common.constant.BpmnConstants.FLOW_NODE_JSON;
public abstract class AbstractBpmnJsonConverter<T extends FlowElement> {
public T convertJsonToElement(BpmnJsonNode node, Process process) {
throw new WorkflowEngineException("暂不支持的节点类型");
throw new WorkflowEngineException(CONVERTOR_NODE_TYPE_NOT_SUPPORT, node.getType().getType());
}
public final void addNodeTypeAttribute(T flowElement, BpmnJsonNode jsonNode) {

View File

@ -4,6 +4,7 @@ import cn.axzo.workflow.common.model.request.bpmn.BpmnNoticeConf;
import cn.axzo.workflow.common.model.request.bpmn.task.BpmnTaskDelegateAssigner;
import cn.axzo.workflow.core.common.exception.WorkflowEngineException;
import static cn.axzo.workflow.core.common.enums.BpmnErrorCode.MES_PUSH_OBJECT_BUILD_ERROR;
import static cn.axzo.workflow.core.engine.event.MessagePushEventType.NOTICE;
import static cn.axzo.workflow.core.engine.event.MessagePushEventType.PENDING_COMPLETE;
import static cn.axzo.workflow.core.engine.event.MessagePushEventType.PENDING_PUSH;
@ -30,7 +31,7 @@ public class MessagePushEventBuilder {
case SMS:
return createSmsEvent(assigner, noticeConf, processInstanceId, tenantId, taskId);
default:
throw new WorkflowEngineException("构造消息推送事件对象异常");
throw new WorkflowEngineException(MES_PUSH_OBJECT_BUILD_ERROR);
}
}

View File

@ -10,6 +10,8 @@ import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.Objects;
import static cn.axzo.workflow.core.common.enums.BpmnErrorCode.ACTIVITY_TRIGGER_NOT_EXISTS;
@Service
@Slf4j
public class BpmnProcessActivityServiceImpl implements BpmnProcessActivityService {
@ -21,7 +23,7 @@ public class BpmnProcessActivityServiceImpl implements BpmnProcessActivityServic
public void trigger(String executionId) {
Execution execution = runtimeService.createExecutionQuery().executionId(executionId).singleResult();
if (Objects.isNull(execution)) {
throw new WorkflowEngineException("业务节点不存在");
throw new WorkflowEngineException(ACTIVITY_TRIGGER_NOT_EXISTS, executionId);
}
runtimeService.trigger(executionId);
}

View File

@ -15,6 +15,8 @@ import javax.annotation.Resource;
import java.util.List;
import java.util.Objects;
import static cn.axzo.workflow.core.common.enums.BpmnErrorCode.MODEL_ID_NOT_EXISTS;
/**
* 模型扩展表操作服务实现
*
@ -50,7 +52,7 @@ public class ExtAxAxReModelServiceImpl implements ExtAxReModelService {
public void changeStatus(String modelId, Integer status) {
ExtAxReModel reModel = extAxReModelMapper.selectOne(new QueryWrapper<ExtAxReModel>().eq("model_id", modelId));
if (Objects.isNull(reModel)) {
throw new WorkflowEngineException("模型不存在");
throw new WorkflowEngineException(MODEL_ID_NOT_EXISTS, reModel.getModelId());
}
reModel.setStatus(status);
extAxReModelMapper.updateById(reModel);

View File

@ -17,6 +17,7 @@
<org.projectlombok.version>1.18.16</org.projectlombok.version>
<org.mapstruct.version>1.4.2.Final</org.mapstruct.version>
<lombok-mapstruct-binding.version>0.2.0</lombok-mapstruct-binding.version>
<redisson.version>3.25.0</redisson.version>
</properties>
<dependencies>
<dependency>
@ -27,10 +28,22 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!--<dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
</dependency>-->
<version>${redisson.version}</version>
<exclusions>
<exclusion>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>${redisson.version}</version>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>workflow-engine-core</artifactId>

View File

@ -22,7 +22,7 @@ public @interface RepeatSubmit {
/**
* 间隔时间(ms)小于此时间视为重复提交
*/
int interval() default 5000;
int interval() default 30000;
TimeUnit timeUnit() default TimeUnit.MILLISECONDS;
@ -32,3 +32,4 @@ public @interface RepeatSubmit {
String message() default "请勿重复提交,正在处理中";
}

View File

@ -0,0 +1,152 @@
package cn.axzo.workflow.server.common.aspectj;
import cn.axzo.workflow.core.common.exception.WorkflowEngineException;
import cn.axzo.workflow.server.common.annotation.RepeatSubmit;
import cn.axzo.workflow.server.common.util.RedisUtils;
import cn.azxo.framework.common.model.CommonResponse;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.crypto.SecureUtil;
import cn.hutool.http.HttpStatus;
import com.alibaba.fastjson.JSON;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.validation.BindingResult;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.time.Duration;
import java.util.Collection;
import java.util.Map;
import java.util.StringJoiner;
import static cn.axzo.workflow.core.common.enums.BpmnErrorCode.REPEAT_SUBMIT_ERROR_TIPS;
/**
* 防止重复提交拦截器
*
* @author wangli
*/
@Aspect
public class RepeatSubmitAspect {
private static final ThreadLocal<String> KEY_CACHE = new ThreadLocal<>();
private static final String REPEAT_SUBMIT_KEY = "global:repeat_submit:";
@Before("@annotation(repeatSubmit)")
public void doBefore(JoinPoint point, RepeatSubmit repeatSubmit) throws Throwable {
// 如果注解不为0 则使用注解数值
long interval = repeatSubmit.timeUnit().toMillis(repeatSubmit.interval());
if (interval < 1000) {
throw new WorkflowEngineException(REPEAT_SUBMIT_ERROR_TIPS, String.valueOf((repeatSubmit.interval() / 1000)));
}
HttpServletRequest request = getRequest();
String nowParams = argsArrayToString(point.getArgs());
// 请求地址作为存放cache的key值
String url = request.getRequestURI();
// 唯一值没有消息头则使用请求地址
String paramsKey = SecureUtil.md5(nowParams);
// 唯一标识指定key + url + 消息头
String cacheRepeatKey = REPEAT_SUBMIT_KEY + url + ":" + paramsKey;
if (RedisUtils.setObjectIfAbsent(cacheRepeatKey, "", Duration.ofMillis(interval))) {
KEY_CACHE.set(cacheRepeatKey);
} else {
throw new WorkflowEngineException(REPEAT_SUBMIT_ERROR_TIPS, repeatSubmit.message());
}
}
/**
* 处理完请求后执行
*
* @param joinPoint 切点
*/
@AfterReturning(pointcut = "@annotation(repeatSubmit)", returning = "jsonResult")
public void doAfterReturning(JoinPoint joinPoint, RepeatSubmit repeatSubmit, Object jsonResult) {
if (jsonResult instanceof CommonResponse) {
CommonResponse<?> r = (CommonResponse<?>) jsonResult;
try {
// 成功则不删除redis数据 保证在有效时间内无法重复提交
if (r.getCode() == HttpStatus.HTTP_OK) {
return;
}
RedisUtils.deleteObject(KEY_CACHE.get());
} finally {
KEY_CACHE.remove();
}
}
}
/**
* 拦截异常操作
*
* @param joinPoint 切点
* @param e 异常
*/
@AfterThrowing(value = "@annotation(repeatSubmit)", throwing = "e")
public void doAfterThrowing(JoinPoint joinPoint, RepeatSubmit repeatSubmit, Exception e) {
RedisUtils.deleteObject(KEY_CACHE.get());
KEY_CACHE.remove();
}
/**
* 参数拼装
*/
private String argsArrayToString(Object[] paramsArray) {
StringJoiner params = new StringJoiner(" ");
if (ArrayUtil.isEmpty(paramsArray)) {
return params.toString();
}
for (Object o : paramsArray) {
if (ObjectUtil.isNotNull(o) && !isFilterObject(o)) {
params.add(JSON.toJSONString(o));
}
}
return params.toString();
}
/**
* 判断是否需要过滤的对象
*
* @param o 对象信息
* @return 如果是需要过滤的对象则返回true否则返回false
*/
@SuppressWarnings("rawtypes")
public boolean isFilterObject(final Object o) {
Class<?> clazz = o.getClass();
if (clazz.isArray()) {
return clazz.getComponentType().isAssignableFrom(MultipartFile.class);
} else if (Collection.class.isAssignableFrom(clazz)) {
Collection collection = (Collection) o;
for (Object value : collection) {
return value instanceof MultipartFile;
}
} else if (Map.class.isAssignableFrom(clazz)) {
Map map = (Map) o;
for (Object value : map.values()) {
return value instanceof MultipartFile;
}
}
return o instanceof MultipartFile || o instanceof HttpServletRequest || o instanceof HttpServletResponse
|| o instanceof BindingResult;
}
public static HttpServletRequest getRequest() {
try {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
return attributes.getRequest();
} catch (Exception e) {
return null;
}
}
}

View File

@ -0,0 +1,79 @@
package cn.axzo.workflow.server.common.config;
import cn.axzo.workflow.server.common.aspectj.RepeatSubmitAspect;
import cn.axzo.workflow.server.common.config.property.RedissonProperties;
import cn.axzo.workflow.server.common.handler.KeyPrefixHandler;
import cn.hutool.core.util.ObjectUtil;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import lombok.extern.slf4j.Slf4j;
import org.redisson.client.codec.StringCodec;
import org.redisson.codec.CompositeCodec;
import org.redisson.codec.TypedJsonJacksonCodec;
import org.redisson.spring.starter.RedissonAutoConfigurationCustomizer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
/**
* redis 配置
*
* @author wangli
* @since 2023/12/12 22:37
*/
@Slf4j
@Component
@EnableConfigurationProperties(RedissonProperties.class)
public class RedisConfiguration {
@Autowired
private RedissonProperties redissonProperties;
@Autowired
private ObjectMapper objectMapper;
@Bean
public RedissonAutoConfigurationCustomizer redissonCustomizer() {
return config -> {
ObjectMapper om = objectMapper.copy();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
// 指定序列化输入的类型类必须是非final修饰的序列化时将对象全类名一起保存下来
om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL);
TypedJsonJacksonCodec jsonCodec = new TypedJsonJacksonCodec(Object.class, om);
// 组合序列化 key 使用 String 内容使用通用 json 格式
CompositeCodec codec = new CompositeCodec(StringCodec.INSTANCE, jsonCodec, jsonCodec);
config.setThreads(redissonProperties.getThreads())
.setNettyThreads(redissonProperties.getNettyThreads())
.setCodec(codec);
RedissonProperties.SingleServerConfig singleServerConfig = redissonProperties.getSingleServerConfig();
if (ObjectUtil.isNotNull(singleServerConfig)) {
// 使用单机模式
config.useSingleServer()
//设置redis key前缀
.setNameMapper(new KeyPrefixHandler(redissonProperties.getKeyPrefix()))
.setTimeout(singleServerConfig.getTimeout())
.setClientName(singleServerConfig.getClientName())
.setIdleConnectionTimeout(singleServerConfig.getIdleConnectionTimeout())
.setSubscriptionConnectionPoolSize(singleServerConfig.getSubscriptionConnectionPoolSize())
.setConnectionMinimumIdleSize(singleServerConfig.getConnectionMinimumIdleSize())
.setConnectionPoolSize(singleServerConfig.getConnectionPoolSize());
}
log.info("初始化 redis 配置");
};
}
@AutoConfigureAfter({RedisConfiguration.class})
public static class IdempotentAutoConfiguration {
@Bean
public RepeatSubmitAspect repeatSubmitAspect() {
return new RepeatSubmitAspect();
}
}
}

View File

@ -1,25 +0,0 @@
package cn.axzo.workflow.server.common.config;
import cn.axzo.workflow.server.common.intercepter.RepeatSubmitInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import javax.annotation.Resource;
/**
* 通用配置
*
* @author wangli
* @since 2023/12/9 22:18
*/
@Configuration
public class ResourcesConfig implements WebMvcConfigurer {
@Resource
private RepeatSubmitInterceptor repeatSubmitInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(repeatSubmitInterceptor).addPathPatterns("/**");
}
}

View File

@ -0,0 +1,71 @@
package cn.axzo.workflow.server.common.config.property;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* Redisson 配置属性
*
* @author wangli
* @since 2023/12/12 22:34
*/
@Data
@ConfigurationProperties(prefix = "redisson")
public class RedissonProperties {
/**
* redis缓存key前缀
*/
private String keyPrefix;
/**
* 线程池数量,默认值 = 当前处理核数量 * 2
*/
private int threads;
/**
* Netty线程池数量,默认值 = 当前处理核数量 * 2
*/
private int nettyThreads;
/**
* 单机服务配置
*/
private SingleServerConfig singleServerConfig;
@Data
@NoArgsConstructor
public static class SingleServerConfig {
/**
* 客户端名称
*/
private String clientName;
/**
* 最小空闲连接数
*/
private int connectionMinimumIdleSize;
/**
* 连接池大小
*/
private int connectionPoolSize;
/**
* 连接空闲超时单位毫秒
*/
private int idleConnectionTimeout;
/**
* 命令等待超时单位毫秒
*/
private int timeout;
/**
* 发布和订阅连接池大小
*/
private int subscriptionConnectionPoolSize;
}
}

View File

@ -0,0 +1,49 @@
package cn.axzo.workflow.server.common.handler;
import org.redisson.api.NameMapper;
import org.springframework.util.StringUtils;
/**
* redis缓存key前缀处理
*
* @author wangli
* @since 2022/7/14 17:44
*/
public class KeyPrefixHandler implements NameMapper {
private final String keyPrefix;
public KeyPrefixHandler(String keyPrefix) {
//前缀为空 则返回空前缀
this.keyPrefix = StringUtils.hasLength(keyPrefix) ? keyPrefix + ":" : "";
}
/**
* 增加前缀
*/
@Override
public String map(String name) {
if (!StringUtils.hasLength(name)) {
return null;
}
if (StringUtils.hasLength(keyPrefix) && !name.startsWith(keyPrefix)) {
return keyPrefix + name;
}
return name;
}
/**
* 去除前缀
*/
@Override
public String unmap(String name) {
if (!StringUtils.hasLength(name)) {
return null;
}
if (StringUtils.hasLength(keyPrefix) && name.startsWith(keyPrefix)) {
return name.substring(keyPrefix.length());
}
return name;
}
}

View File

@ -1,60 +0,0 @@
package cn.axzo.workflow.server.common.intercepter;
import cn.axzo.workflow.server.common.annotation.RepeatSubmit;
import cn.azxo.framework.common.model.CommonResponse;
import com.alibaba.fastjson.JSON;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.lang.reflect.Method;
/**
* 防止重复提交拦截器
*
* @author wangli
*/
@Component
public abstract class RepeatSubmitInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (handler instanceof HandlerMethod) {
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
RepeatSubmit annotation = method.getAnnotation(RepeatSubmit.class);
if (annotation != null) {
if (this.isRepeatSubmit(request, annotation)) {
CommonResponse commonResponse = CommonResponse.error(annotation.message());
renderString(response, JSON.toJSONString(commonResponse));
return false;
}
}
return true;
} else {
return true;
}
}
/**
* 验证是否重复提交由子类实现具体的防重复提交的规则
*
* @param request 请求对象
* @param annotation 防复注解
* @return 结果
*/
public abstract boolean isRepeatSubmit(HttpServletRequest request, RepeatSubmit annotation) throws Exception;
private static String renderString(HttpServletResponse response, String string) {
try {
response.setContentType("application/json");
response.setCharacterEncoding("utf-8");
response.getWriter().print(string);
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
}

View File

@ -1,79 +0,0 @@
package cn.axzo.workflow.server.common.intercepter.impl;
import cn.axzo.workflow.server.common.annotation.RepeatSubmit;
import cn.axzo.workflow.server.common.intercepter.RepeatSubmitInterceptor;
import com.alibaba.fastjson.JSON;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import java.util.HashMap;
import java.util.Map;
/**
* 判断请求url和数据是否和上一次相同
* 如果和上次相同则是重复提交表单 有效时间为10秒内
*
* @author ruoyi
*/
@Component
public class SameUrlDataInterceptor extends RepeatSubmitInterceptor {
public final String REPEAT_PARAMS = "repeatParams";
public final String REPEAT_TIME = "repeatTime";
public final String SESSION_REPEAT_KEY = "repeatData";
// @Resource
// private StringRedisTemplate redisTemplate;
@SuppressWarnings("unchecked")
@Override
public boolean isRepeatSubmit(HttpServletRequest request, RepeatSubmit annotation) throws Exception {
// 本次参数及系统时间
String nowParams = JSON.toJSONString(request.getParameterMap());
Map<String, Object> nowDataMap = new HashMap<String, Object>();
nowDataMap.put(REPEAT_PARAMS, nowParams);
nowDataMap.put(REPEAT_TIME, System.currentTimeMillis());
// 请求地址作为存放session的key值
String url = request.getRequestURI();
HttpSession session = request.getSession();
Object sessionObj = session.getAttribute(SESSION_REPEAT_KEY);
if (sessionObj != null) {
Map<String, Object> sessionMap = (Map<String, Object>) sessionObj;
if (sessionMap.containsKey(url)) {
Map<String, Object> preDataMap = (Map<String, Object>) sessionMap.get(url);
if (compareParams(nowDataMap, preDataMap) && compareTime(nowDataMap, preDataMap, annotation.interval())) {
return true;
}
}
}
Map<String, Object> sessionMap = new HashMap<String, Object>();
sessionMap.put(url, nowDataMap);
session.setAttribute(SESSION_REPEAT_KEY, sessionMap);
return false;
}
/**
* 判断参数是否相同
*/
private boolean compareParams(Map<String, Object> nowMap, Map<String, Object> preMap) {
String nowParams = (String) nowMap.get(REPEAT_PARAMS);
String preParams = (String) preMap.get(REPEAT_PARAMS);
return nowParams.equals(preParams);
}
/**
* 判断两次间隔时间
*/
private boolean compareTime(Map<String, Object> nowMap, Map<String, Object> preMap, int interval) {
long time1 = (Long) nowMap.get(REPEAT_TIME);
long time2 = (Long) preMap.get(REPEAT_TIME);
if ((time1 - time2) < interval) {
return true;
}
return false;
}
}

View File

@ -0,0 +1,501 @@
package cn.axzo.workflow.server.common.util;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.redisson.api.ObjectListener;
import org.redisson.api.RAtomicLong;
import org.redisson.api.RBatch;
import org.redisson.api.RBucket;
import org.redisson.api.RBucketAsync;
import org.redisson.api.RKeys;
import org.redisson.api.RList;
import org.redisson.api.RMap;
import org.redisson.api.RMapAsync;
import org.redisson.api.RRateLimiter;
import org.redisson.api.RSet;
import org.redisson.api.RTopic;
import org.redisson.api.RateIntervalUnit;
import org.redisson.api.RateType;
import org.redisson.api.RedissonClient;
import java.time.Duration;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* redis 工具包
*
* @author wangli
*/
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@SuppressWarnings(value = {"unchecked", "rawtypes"})
public class RedisUtils {
private static final RedissonClient CLIENT = SpringUtils.getBean(RedissonClient.class);
/**
* 限流
*
* @param key 限流key
* @param rateType 限流类型
* @param rate 速率
* @param rateInterval 速率间隔
* @return -1 表示失败
*/
public static long rateLimiter(String key, RateType rateType, int rate, int rateInterval) {
RRateLimiter rateLimiter = CLIENT.getRateLimiter(key);
rateLimiter.trySetRate(rateType, rate, rateInterval, RateIntervalUnit.SECONDS);
if (rateLimiter.tryAcquire()) {
return rateLimiter.availablePermits();
} else {
return -1L;
}
}
/**
* 获取客户端实例
*/
public static RedissonClient getClient() {
return CLIENT;
}
/**
* 发布通道消息
*
* @param channelKey 通道key
* @param msg 发送数据
* @param consumer 自定义处理
*/
public static <T> void publish(String channelKey, T msg, Consumer<T> consumer) {
RTopic topic = CLIENT.getTopic(channelKey);
topic.publish(msg);
consumer.accept(msg);
}
public static <T> void publish(String channelKey, T msg) {
RTopic topic = CLIENT.getTopic(channelKey);
topic.publish(msg);
}
/**
* 订阅通道接收消息
*
* @param channelKey 通道key
* @param clazz 消息类型
* @param consumer 自定义处理
*/
public static <T> void subscribe(String channelKey, Class<T> clazz, Consumer<T> consumer) {
RTopic topic = CLIENT.getTopic(channelKey);
topic.addListener(clazz, (channel, msg) -> consumer.accept(msg));
}
/**
* 缓存基本的对象IntegerString实体类等
*
* @param key 缓存的键值
* @param value 缓存的值
*/
public static <T> void setCacheObject(final String key, final T value) {
setCacheObject(key, value, false);
}
/**
* 缓存基本的对象保留当前对象 TTL 有效期
*
* @param key 缓存的键值
* @param value 缓存的值
* @param isSaveTtl 是否保留TTL有效期(例如: set之前ttl剩余90 set之后还是为90)
* @since Redis 6.X 以上使用 setAndKeepTTL 兼容 5.X 方案
*/
public static <T> void setCacheObject(final String key, final T value, final boolean isSaveTtl) {
RBucket<T> bucket = CLIENT.getBucket(key);
if (isSaveTtl) {
try {
bucket.setAndKeepTTL(value);
} catch (Exception e) {
long timeToLive = bucket.remainTimeToLive();
setCacheObject(key, value, Duration.ofMillis(timeToLive));
}
} else {
bucket.set(value);
}
}
/**
* 缓存基本的对象IntegerString实体类等
*
* @param key 缓存的键值
* @param value 缓存的值
* @param duration 时间
*/
public static <T> void setCacheObject(final String key, final T value, final Duration duration) {
RBatch batch = CLIENT.createBatch();
RBucketAsync<T> bucket = batch.getBucket(key);
bucket.setAsync(value);
bucket.expireAsync(duration);
batch.execute();
}
/**
* 如果不存在则设置 并返回 true 如果存在则返回 false
*
* @param key 缓存的键值
* @param value 缓存的值
* @return set成功或失败
*/
public static <T> boolean setObjectIfAbsent(final String key, final T value, final Duration duration) {
RBucket<T> bucket = CLIENT.getBucket(key);
return bucket.setIfAbsent(value, duration);
}
/**
* 注册对象监听器
* <p>
* key 监听器需开启 `notify-keyspace-events` redis 相关配置
*
* @param key 缓存的键值
* @param listener 监听器配置
*/
public static <T> void addObjectListener(final String key, final ObjectListener listener) {
RBucket<T> result = CLIENT.getBucket(key);
result.addListener(listener);
}
/**
* 设置有效时间
*
* @param key Redis键
* @param timeout 超时时间
* @return true=设置成功false=设置失败
*/
public static boolean expire(final String key, final long timeout) {
return expire(key, Duration.ofSeconds(timeout));
}
/**
* 设置有效时间
*
* @param key Redis键
* @param duration 超时时间
* @return true=设置成功false=设置失败
*/
public static boolean expire(final String key, final Duration duration) {
RBucket rBucket = CLIENT.getBucket(key);
return rBucket.expire(duration);
}
/**
* 获得缓存的基本对象
*
* @param key 缓存键值
* @return 缓存键值对应的数据
*/
public static <T> T getCacheObject(final String key) {
RBucket<T> rBucket = CLIENT.getBucket(key);
return rBucket.get();
}
/**
* 获得key剩余存活时间
*
* @param key 缓存键值
* @return 剩余存活时间
*/
public static <T> long getTimeToLive(final String key) {
RBucket<T> rBucket = CLIENT.getBucket(key);
return rBucket.remainTimeToLive();
}
/**
* 删除单个对象
*
* @param key 缓存的键值
*/
public static boolean deleteObject(final String key) {
return CLIENT.getBucket(key).delete();
}
/**
* 删除集合对象
*
* @param collection 多个对象
*/
public static void deleteObject(final Collection collection) {
RBatch batch = CLIENT.createBatch();
collection.forEach(t -> {
batch.getBucket(t.toString()).deleteAsync();
});
batch.execute();
}
/**
* 检查缓存对象是否存在
*
* @param key 缓存的键值
*/
public static boolean isExistsObject(final String key) {
return CLIENT.getBucket(key).isExists();
}
/**
* 缓存List数据
*
* @param key 缓存的键值
* @param dataList 待缓存的List数据
* @return 缓存的对象
*/
public static <T> boolean setCacheList(final String key, final List<T> dataList) {
RList<T> rList = CLIENT.getList(key);
return rList.addAll(dataList);
}
/**
* 注册List监听器
* <p>
* key 监听器需开启 `notify-keyspace-events` redis 相关配置
*
* @param key 缓存的键值
* @param listener 监听器配置
*/
public static <T> void addListListener(final String key, final ObjectListener listener) {
RList<T> rList = CLIENT.getList(key);
rList.addListener(listener);
}
/**
* 获得缓存的list对象
*
* @param key 缓存的键值
* @return 缓存键值对应的数据
*/
public static <T> List<T> getCacheList(final String key) {
RList<T> rList = CLIENT.getList(key);
return rList.readAll();
}
/**
* 缓存Set
*
* @param key 缓存键值
* @param dataSet 缓存的数据
* @return 缓存数据的对象
*/
public static <T> boolean setCacheSet(final String key, final Set<T> dataSet) {
RSet<T> rSet = CLIENT.getSet(key);
return rSet.addAll(dataSet);
}
/**
* 注册Set监听器
* <p>
* key 监听器需开启 `notify-keyspace-events` redis 相关配置
*
* @param key 缓存的键值
* @param listener 监听器配置
*/
public static <T> void addSetListener(final String key, final ObjectListener listener) {
RSet<T> rSet = CLIENT.getSet(key);
rSet.addListener(listener);
}
/**
* 获得缓存的set
*
* @param key 缓存的key
* @return set对象
*/
public static <T> Set<T> getCacheSet(final String key) {
RSet<T> rSet = CLIENT.getSet(key);
return rSet.readAll();
}
/**
* 缓存Map
*
* @param key 缓存的键值
* @param dataMap 缓存的数据
*/
public static <T> void setCacheMap(final String key, final Map<String, T> dataMap) {
if (dataMap != null) {
RMap<String, T> rMap = CLIENT.getMap(key);
rMap.putAll(dataMap);
}
}
/**
* 注册Map监听器
* <p>
* key 监听器需开启 `notify-keyspace-events` redis 相关配置
*
* @param key 缓存的键值
* @param listener 监听器配置
*/
public static <T> void addMapListener(final String key, final ObjectListener listener) {
RMap<String, T> rMap = CLIENT.getMap(key);
rMap.addListener(listener);
}
/**
* 获得缓存的Map
*
* @param key 缓存的键值
* @return map对象
*/
public static <T> Map<String, T> getCacheMap(final String key) {
RMap<String, T> rMap = CLIENT.getMap(key);
return rMap.getAll(rMap.keySet());
}
/**
* 获得缓存Map的key列表
*
* @param key 缓存的键值
* @return key列表
*/
public static <T> Set<String> getCacheMapKeySet(final String key) {
RMap<String, T> rMap = CLIENT.getMap(key);
return rMap.keySet();
}
/**
* 往Hash中存入数据
*
* @param key Redis键
* @param hKey Hash键
* @param value
*/
public static <T> void setCacheMapValue(final String key, final String hKey, final T value) {
RMap<String, T> rMap = CLIENT.getMap(key);
rMap.put(hKey, value);
}
/**
* 获取Hash中的数据
*
* @param key Redis键
* @param hKey Hash键
* @return Hash中的对象
*/
public static <T> T getCacheMapValue(final String key, final String hKey) {
RMap<String, T> rMap = CLIENT.getMap(key);
return rMap.get(hKey);
}
/**
* 删除Hash中的数据
*
* @param key Redis键
* @param hKey Hash键
* @return Hash中的对象
*/
public static <T> T delCacheMapValue(final String key, final String hKey) {
RMap<String, T> rMap = CLIENT.getMap(key);
return rMap.remove(hKey);
}
/**
* 删除Hash中的数据
*
* @param key Redis键
* @param hKeys Hash键
*/
public static <T> void delMultiCacheMapValue(final String key, final Set<String> hKeys) {
RBatch batch = CLIENT.createBatch();
RMapAsync<String, T> rMap = batch.getMap(key);
for (String hKey : hKeys) {
rMap.removeAsync(hKey);
}
batch.execute();
}
/**
* 获取多个Hash中的数据
*
* @param key Redis键
* @param hKeys Hash键集合
* @return Hash对象集合
*/
public static <K, V> Map<K, V> getMultiCacheMapValue(final String key, final Set<K> hKeys) {
RMap<K, V> rMap = CLIENT.getMap(key);
return rMap.getAll(hKeys);
}
/**
* 设置原子值
*
* @param key Redis键
* @param value
*/
public static void setAtomicValue(String key, long value) {
RAtomicLong atomic = CLIENT.getAtomicLong(key);
atomic.set(value);
}
/**
* 获取原子值
*
* @param key Redis键
* @return 当前值
*/
public static long getAtomicValue(String key) {
RAtomicLong atomic = CLIENT.getAtomicLong(key);
return atomic.get();
}
/**
* 递增原子值
*
* @param key Redis键
* @return 当前值
*/
public static long incrAtomicValue(String key) {
RAtomicLong atomic = CLIENT.getAtomicLong(key);
return atomic.incrementAndGet();
}
/**
* 递减原子值
*
* @param key Redis键
* @return 当前值
*/
public static long decrAtomicValue(String key) {
RAtomicLong atomic = CLIENT.getAtomicLong(key);
return atomic.decrementAndGet();
}
/**
* 获得缓存的基本对象列表
*
* @param pattern 字符串前缀
* @return 对象列表
*/
public static Collection<String> keys(final String pattern) {
Stream<String> stream = CLIENT.getKeys().getKeysStreamByPattern(pattern);
return stream.collect(Collectors.toList());
}
/**
* 删除缓存的基本对象列表
*
* @param pattern 字符串前缀
*/
public static void deleteKeys(final String pattern) {
CLIENT.getKeys().deleteByPattern(pattern);
}
/**
* 检查redis中是否存在key
*
* @param key
*/
public static Boolean hasKey(String key) {
RKeys rKeys = CLIENT.getKeys();
return rKeys.countExists(key) > 0;
}
}

View File

@ -0,0 +1,282 @@
package cn.axzo.workflow.server.common.util;
import cn.hutool.core.exceptions.UtilException;
import cn.hutool.core.lang.TypeReference;
import cn.hutool.core.util.ArrayUtil;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.DefaultSingletonBeanRegistry;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.ResolvableType;
import org.springframework.stereotype.Component;
import java.lang.reflect.ParameterizedType;
import java.util.Arrays;
import java.util.Map;
/**
* Spring 工具包
*
* @author wangli
* @since 2023/12/12 22:51
*/
@Component
public class SpringUtils implements BeanFactoryPostProcessor, ApplicationContextAware {
/**
* "@PostConstruct"注解标记的类中由于ApplicationContext还未加载导致空指针<br>
* 因此实现BeanFactoryPostProcessor注入ConfigurableListableBeanFactory实现bean的操作
*/
private static ConfigurableListableBeanFactory beanFactory;
/**
* Spring应用上下文环境
*/
private static ApplicationContext applicationContext;
@SuppressWarnings("NullableProblems")
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
this.beanFactory = beanFactory;
}
@SuppressWarnings("NullableProblems")
@Override
public void setApplicationContext(ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}
/**
* 获取{@link ApplicationContext}
*
* @return {@link ApplicationContext}
*/
public static ApplicationContext getApplicationContext() {
return applicationContext;
}
/**
* 获取{@link ListableBeanFactory}可能为{@link ConfigurableListableBeanFactory} {@link ApplicationContextAware}
*
* @return {@link ListableBeanFactory}
* @since 5.7.0
*/
public static ListableBeanFactory getBeanFactory() {
final ListableBeanFactory factory = null == beanFactory ? applicationContext : beanFactory;
if (null == factory) {
throw new UtilException("No ConfigurableListableBeanFactory or ApplicationContext injected, maybe not in the Spring environment?");
}
return factory;
}
/**
* 获取{@link ConfigurableListableBeanFactory}
*
* @return {@link ConfigurableListableBeanFactory}
* @throws UtilException 当上下文非ConfigurableListableBeanFactory抛出异常
* @since 5.7.7
*/
public static ConfigurableListableBeanFactory getConfigurableBeanFactory() throws UtilException {
final ConfigurableListableBeanFactory factory;
if (null != beanFactory) {
factory = beanFactory;
} else if (applicationContext instanceof ConfigurableApplicationContext) {
factory = ((ConfigurableApplicationContext) applicationContext).getBeanFactory();
} else {
throw new UtilException("No ConfigurableListableBeanFactory from context!");
}
return factory;
}
//通过name获取 Bean.
/**
* 通过name获取 Bean
*
* @param <T> Bean类型
* @param name Bean名称
* @return Bean
*/
@SuppressWarnings("unchecked")
public static <T> T getBean(String name) {
return (T) getBeanFactory().getBean(name);
}
/**
* 通过class获取Bean
*
* @param <T> Bean类型
* @param clazz Bean类
* @return Bean对象
*/
public static <T> T getBean(Class<T> clazz) {
return getBeanFactory().getBean(clazz);
}
/**
* 通过name,以及Clazz返回指定的Bean
*
* @param <T> bean类型
* @param name Bean名称
* @param clazz bean类型
* @return Bean对象
*/
public static <T> T getBean(String name, Class<T> clazz) {
return getBeanFactory().getBean(name, clazz);
}
/**
* 通过类型参考返回带泛型参数的Bean
*
* @param reference 类型参考用于持有转换后的泛型类型
* @param <T> Bean类型
* @return 带泛型参数的Bean
* @since 5.4.0
*/
@SuppressWarnings("unchecked")
public static <T> T getBean(TypeReference<T> reference) {
final ParameterizedType parameterizedType = (ParameterizedType) reference.getType();
final Class<T> rawType = (Class<T>) parameterizedType.getRawType();
final Class<?>[] genericTypes = Arrays.stream(parameterizedType.getActualTypeArguments()).map(type -> (Class<?>) type).toArray(Class[]::new);
final String[] beanNames = getBeanFactory().getBeanNamesForType(ResolvableType.forClassWithGenerics(rawType, genericTypes));
return getBean(beanNames[0], rawType);
}
/**
* 获取指定类型对应的所有Bean包括子类
*
* @param <T> Bean类型
* @param type 接口null表示获取所有bean
* @return 类型对应的beankey是bean注册的namevalue是Bean
* @since 5.3.3
*/
public static <T> Map<String, T> getBeansOfType(Class<T> type) {
return getBeanFactory().getBeansOfType(type);
}
/**
* 获取指定类型对应的Bean名称包括子类
*
* @param type 接口null表示获取所有bean名称
* @return bean名称
* @since 5.3.3
*/
public static String[] getBeanNamesForType(Class<?> type) {
return getBeanFactory().getBeanNamesForType(type);
}
/**
* 获取配置文件配置项的值
*
* @param key 配置项key
* @return 属性值
* @since 5.3.3
*/
public static String getProperty(String key) {
if (null == applicationContext) {
return null;
}
return applicationContext.getEnvironment().getProperty(key);
}
/**
* 获取应用程序名称
*
* @return 应用程序名称
* @since 5.7.12
*/
public static String getApplicationName() {
return getProperty("spring.application.name");
}
/**
* 获取当前的环境配置无配置返回null
*
* @return 当前的环境配置
* @since 5.3.3
*/
public static String[] getActiveProfiles() {
if (null == applicationContext) {
return null;
}
return applicationContext.getEnvironment().getActiveProfiles();
}
/**
* 获取当前的环境配置当有多个环境配置时只获取第一个
*
* @return 当前的环境配置
* @since 5.3.3
*/
public static String getActiveProfile() {
final String[] activeProfiles = getActiveProfiles();
return ArrayUtil.isNotEmpty(activeProfiles) ? activeProfiles[0] : null;
}
/**
* 动态向Spring注册Bean
* <p>
* {@link org.springframework.beans.factory.BeanFactory} 实现通过工具开放API
* <p>
* 更新: shadow 2021-07-29 17:20:44 增加自动注入修复注册bean无法反向注入的问题
*
* @param <T> Bean类型
* @param beanName 名称
* @param bean bean
* @author shadow
* @since 5.4.2
*/
public static <T> void registerBean(String beanName, T bean) {
final ConfigurableListableBeanFactory factory = getConfigurableBeanFactory();
factory.autowireBean(bean);
factory.registerSingleton(beanName, bean);
}
/**
* 注销bean
* <p>
* 将Spring中的bean注销请谨慎使用
*
* @param beanName bean名称
* @author shadow
* @since 5.7.7
*/
public static void unregisterBean(String beanName) {
final ConfigurableListableBeanFactory factory = getConfigurableBeanFactory();
if (factory instanceof DefaultSingletonBeanRegistry) {
DefaultSingletonBeanRegistry registry = (DefaultSingletonBeanRegistry) factory;
registry.destroySingleton(beanName);
} else {
throw new UtilException("Can not unregister bean, the factory is not a DefaultSingletonBeanRegistry!");
}
}
/**
* 发布事件
*
* @param event 待发布的事件事件必须是{@link ApplicationEvent}的子类
* @since 5.7.12
*/
public static void publishEvent(ApplicationEvent event) {
if (null != applicationContext) {
applicationContext.publishEvent(event);
}
}
/**
* 发布事件
* Spring 4.2+ 版本事件可以不再是{@link ApplicationEvent}的子类
*
* @param event 待发布的事件
* @since 5.7.21
*/
public static void publishEvent(Object event) {
if (null != applicationContext) {
applicationContext.publishEvent(event);
}
}
}

View File

@ -24,6 +24,7 @@ import java.util.Objects;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import static cn.axzo.workflow.core.common.enums.BpmnErrorCode.CALC_TASK_ASSIGNEE_ERROR;
import static cn.axzo.workflow.core.common.enums.BpmnErrorCode.CONVERTOR_META_DATA_FORMAT_ERROR;
import static cn.axzo.workflow.core.common.enums.BpmnErrorCode.ENGINE_USER_TASK_CALC_ERROR;
@ -74,7 +75,7 @@ public abstract class AbstractBpmnTaskAssigneeSelector implements BpmnTaskAssign
Assert.notNull(result, "服务调用异常");
// 200自定义处理
if (HttpStatus.HTTP_OK != result.getCode()) {
throw new WorkflowEngineException("执行计算审批候选人出现异常: " + result.getMsg());
throw new WorkflowEngineException(CALC_TASK_ASSIGNEE_ERROR, result.getMsg());
}
return result.getData();
}

View File

@ -9,6 +9,8 @@ import org.springframework.stereotype.Component;
import java.util.List;
import static cn.axzo.workflow.core.common.enums.BpmnErrorCode.ENGINE_USER_TASK_TYPE_NOT_SUPPORT;
/**
* todo 本期需求不实现
* 基于"发起人多级主管"查询审批人
@ -26,7 +28,7 @@ public class InitiatorLeaderRecursionTaskAssigneeSelector extends AbstractBpmnTa
@Override
public List<BpmnTaskDelegateAssigner> select(UserTask userTask, DelegateExecution execution,
Boolean throwException) {
throw new WorkflowEngineException("暂不支持");
throw new WorkflowEngineException(ENGINE_USER_TASK_TYPE_NOT_SUPPORT);
}
}