add - 为常用的接口添加防重提交限制功能
This commit is contained in:
parent
155e76af47
commit
7a369a42a9
@ -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 ==========
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 "请勿重复提交,正在处理中";
|
||||
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@ -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("/**");
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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));
|
||||
}
|
||||
|
||||
/**
|
||||
* 缓存基本的对象,Integer、String、实体类等
|
||||
*
|
||||
* @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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 缓存基本的对象,Integer、String、实体类等
|
||||
*
|
||||
* @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;
|
||||
}
|
||||
}
|
||||
@ -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 类型对应的bean,key是bean注册的name,value是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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user