Просмотр исходного кода

添加公共模块,接口幂等性,去除重复的请求

mac 2 лет назад
Родитель
Сommit
1116e42acf

+ 4 - 4
kxs-common/kxs-common-core/src/main/java/com/kxs/common/core/exception/GlobalBizExceptionHandler.java

@@ -140,13 +140,13 @@ public class GlobalBizExceptionHandler {
 
 
 	/**
 	/**
 	 * 自定义异常
 	 * 自定义异常
-	 * @param e 错误类型
+	 * @param exception 错误类型
 	 * @return {@link R}
 	 * @return {@link R}
 	 */
 	 */
 	@ExceptionHandler(GlobalCustomerException.class)
 	@ExceptionHandler(GlobalCustomerException.class)
-	public R handlerCustomException(GlobalCustomerException e){
-
-		return R.failed(e.getMessage());
+	public R handlerCustomException(GlobalCustomerException exception){
+		log.info("自定义异常:{}", exception.getMessage());
+		return R.failed(exception.getMessage());
 	}
 	}
 
 
 	/**
 	/**

+ 56 - 0
kxs-common/kxs-common-idempotent/pom.xml

@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>com.kxs</groupId>
+        <artifactId>kxs-common</artifactId>
+        <version>1.1.0</version>
+    </parent>
+
+    <artifactId>kxs-common-idempotent</artifactId>
+
+    <properties>
+        <maven.compiler.source>17</maven.compiler.source>
+        <maven.compiler.target>17</maven.compiler.target>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+        <mica.version>3.0.0</mica.version>
+<!--        <redisson.version>3.24.3</redisson.version>-->
+        <redisson.version>3.16.2</redisson.version>
+    </properties>
+    <dependencies>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-web</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.kxs</groupId>
+            <artifactId>kxs-common-core</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-test</artifactId>
+            <scope>test</scope>
+        </dependency>
+
+        <!--aop-->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-aop</artifactId>
+        </dependency>
+        <!--redisson-->
+        <dependency>
+            <groupId>org.redisson</groupId>
+            <artifactId>redisson-spring-boot-starter</artifactId>
+            <version>${redisson.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>net.dreamlu</groupId>
+            <artifactId>mica-auto</artifactId>
+            <version>${mica.version}</version>
+            <scope>provided</scope>
+        </dependency>
+    </dependencies>
+</project>

+ 58 - 0
kxs-common/kxs-common-idempotent/src/main/java/com/kxs/common/idempotent/IdempotentAutoConfiguration.java

@@ -0,0 +1,58 @@
+package com.kxs.common.idempotent;
+
+import com.kxs.common.idempotent.aspect.IdempotentAspect;
+import com.kxs.common.idempotent.expression.ExpressionResolver;
+import com.kxs.common.idempotent.expression.KeyResolver;
+import org.redisson.Redisson;
+import org.redisson.api.RedissonClient;
+import org.redisson.config.Config;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.AutoConfigureAfter;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * @author lengleng
+ * @date 2020/9/25
+ * <p>
+ * 幂等插件初始化
+ */
+@AutoConfiguration
+@Configuration(proxyBeanMethods = false)
+@AutoConfigureAfter(RedisAutoConfiguration.class)
+public class IdempotentAutoConfiguration {
+
+	@Value("${spring.data.redis.host}")
+	private String host;
+
+	/**
+	 * 切面 拦截处理所有 @Idempotent
+	 * @return Aspect
+	 */
+	@Bean
+	public IdempotentAspect idempotentAspect() {
+		return new IdempotentAspect();
+	}
+
+	/**
+	 * key 解析器
+	 * @return KeyResolver
+	 */
+	@Bean
+	@ConditionalOnMissingBean(KeyResolver.class)
+	public KeyResolver keyResolver() {
+		return new ExpressionResolver();
+	}
+
+	@Bean
+	@ConditionalOnMissingBean
+	public RedissonClient redissonClient() {
+		Config config = new Config();
+		config.useSingleServer().setAddress("redis://"+host+":6379");
+		return Redisson.create(config);
+	}
+
+}

+ 51 - 0
kxs-common/kxs-common-idempotent/src/main/java/com/kxs/common/idempotent/annotation/Idempotent.java

@@ -0,0 +1,51 @@
+package com.kxs.common.idempotent.annotation;
+
+import java.lang.annotation.*;
+import java.util.concurrent.TimeUnit;
+
+
+/**
+ * 接口幂等
+ *
+ * @author 没秃顶的码农
+ * @date 2024-04-26
+ */
+@Inherited
+@Target(ElementType.METHOD)
+@Retention(value = RetentionPolicy.RUNTIME)
+public @interface Idempotent {
+
+	/**
+	 * <p>
+	 * 如果是实体类的话,默认拦截不会生效. objects.toString()会返回不同地址.
+	 * </p>
+	 * 幂等操作的唯一标识,使用spring el表达式 用#来引用方法参数
+	 * @return Spring-EL expression
+	 */
+	String key() default "";
+
+	/**
+	 * 有效期 默认:1 有效期要大于程序执行时间,否则请求还是可能会进来
+	 * @return expireTime
+	 */
+	int expireTime() default 1;
+
+	/**
+	 * 时间单位 默认:s
+	 * @return TimeUnit
+	 */
+	TimeUnit timeUnit() default TimeUnit.SECONDS;
+
+	/**
+	 * 提示信息,可自定义
+	 * @return String
+	 */
+	String info() default "重复请求,请稍后重试";
+
+	/**
+	 * 是否在业务完成后删除key true:删除 false:不删除
+	 * @return boolean
+	 */
+	boolean delKey() default false;
+
+}

+ 142 - 0
kxs-common/kxs-common-idempotent/src/main/java/com/kxs/common/idempotent/aspect/IdempotentAspect.java

@@ -0,0 +1,142 @@
+package com.kxs.common.idempotent.aspect;
+
+import com.kxs.common.core.exception.GlobalCustomerException;
+import com.kxs.common.idempotent.annotation.Idempotent;
+import com.kxs.common.idempotent.exception.IdempotentException;
+import com.kxs.common.idempotent.expression.KeyResolver;
+import jakarta.servlet.http.HttpServletRequest;
+import org.aspectj.lang.JoinPoint;
+import org.aspectj.lang.annotation.After;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.annotation.Before;
+import org.aspectj.lang.annotation.Pointcut;
+import org.aspectj.lang.reflect.MethodSignature;
+import org.redisson.api.RMapCache;
+import org.redisson.api.RedissonClient;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.util.CollectionUtils;
+import org.springframework.util.StringUtils;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+
+import java.lang.reflect.Method;
+import java.time.LocalDateTime;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+
+/**
+ * The Idempotent Aspect
+ *
+ * @author 没秃顶的码农
+ * @date 2024-04-26
+ */
+@Aspect
+public class IdempotentAspect {
+
+	private static final Logger LOGGER = LoggerFactory.getLogger(IdempotentAspect.class);
+
+	private static final ThreadLocal<Map<String, Object>> THREAD_CACHE = ThreadLocal.withInitial(HashMap::new);
+
+	private static final String RMAPCACHE_KEY = "idempotent";
+
+	private static final String KEY = "key";
+
+	private static final String DELKEY = "delKey";
+
+	@Autowired
+	private RedissonClient redissonClient;
+
+	@Autowired
+	private KeyResolver keyResolver;
+
+	@Pointcut("@annotation(com.kxs.common.idempotent.annotation.Idempotent)")
+	public void pointCut() {
+	}
+
+	@Before("pointCut()")
+	public void beforePointCut(JoinPoint joinPoint) {
+		ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder
+				.getRequestAttributes();
+		HttpServletRequest request = requestAttributes.getRequest();
+
+		MethodSignature signature = (MethodSignature) joinPoint.getSignature();
+		Method method = signature.getMethod();
+		if (!method.isAnnotationPresent(Idempotent.class)) {
+			return;
+		}
+		Idempotent idempotent = method.getAnnotation(Idempotent.class);
+
+		String key;
+
+		// 若没有配置 幂等 标识编号,则使用 url + 参数列表作为区分
+		if (!StringUtils.hasLength(idempotent.key())) {
+			String url = request.getRequestURL().toString();
+			String argString = Arrays.asList(joinPoint.getArgs()).toString();
+			key = url + argString;
+		}
+		else {
+			// 使用jstl 规则区分
+			key = keyResolver.resolver(idempotent, joinPoint);
+		}
+		// 当配置了el表达式但是所选字段为空时,会抛出异常,兜底使用url做标识
+		if (key == null) {
+			key = request.getRequestURL().toString();
+		}
+
+		long expireTime = idempotent.expireTime();
+		String info = idempotent.info();
+		TimeUnit timeUnit = idempotent.timeUnit();
+		boolean delKey = idempotent.delKey();
+
+		// do not need check null
+		RMapCache<String, Object> rMapCache = redissonClient.getMapCache(RMAPCACHE_KEY);
+		String value = LocalDateTime.now().toString().replace("T", " ");
+		Object v1;
+		if (null != rMapCache.get(key)) {
+			// 自定义异常
+			throw new GlobalCustomerException(info);
+		}
+		synchronized (this) {
+			v1 = rMapCache.putIfAbsent(key, value, expireTime, timeUnit);
+			if (null != v1) {
+				throw new IdempotentException(info);
+			}
+			else {
+				LOGGER.info("[idempotent]:has stored key={},value={},expireTime={}{},now={}", key, value, expireTime,
+						timeUnit, LocalDateTime.now().toString());
+			}
+		}
+
+		Map<String, Object> map = THREAD_CACHE.get();
+		map.put(KEY, key);
+		map.put(DELKEY, delKey);
+	}
+
+	@After("pointCut()")
+	public void afterPointCut(JoinPoint joinPoint) {
+		Map<String, Object> map = THREAD_CACHE.get();
+		if (CollectionUtils.isEmpty(map)) {
+			return;
+		}
+
+		RMapCache<Object, Object> mapCache = redissonClient.getMapCache(RMAPCACHE_KEY);
+		if (mapCache.size() == 0) {
+			return;
+		}
+
+		String key = map.get(KEY).toString();
+		boolean delKey = (boolean) map.get(DELKEY);
+
+		if (delKey) {
+			mapCache.fastRemove(key);
+			LOGGER.info("[idempotent]:has removed key={}", key);
+		}
+		THREAD_CACHE.remove();
+	}
+
+}

+ 33 - 0
kxs-common/kxs-common-idempotent/src/main/java/com/kxs/common/idempotent/exception/IdempotentException.java

@@ -0,0 +1,33 @@
+package com.kxs.common.idempotent.exception;
+
+
+/**
+ * 幂等异常
+ *
+ * @author 没秃顶的码农
+ * @date 2024-04-26
+ */
+public class IdempotentException extends RuntimeException {
+
+	public IdempotentException() {
+		super();
+	}
+
+	public IdempotentException(String message) {
+		super(message);
+	}
+
+	public IdempotentException(String message, Throwable cause) {
+		super(message, cause);
+	}
+
+	public IdempotentException(Throwable cause) {
+		super(cause);
+	}
+
+	protected IdempotentException(String message, Throwable cause, boolean enableSuppression,
+			boolean writableStackTrace) {
+		super(message, cause, enableSuppression, writableStackTrace);
+	}
+
+}

+ 66 - 0
kxs-common/kxs-common-idempotent/src/main/java/com/kxs/common/idempotent/expression/ExpressionResolver.java

@@ -0,0 +1,66 @@
+package com.kxs.common.idempotent.expression;
+
+
+
+import com.kxs.common.idempotent.annotation.Idempotent;
+import org.aspectj.lang.JoinPoint;
+import org.aspectj.lang.reflect.MethodSignature;
+import org.springframework.core.DefaultParameterNameDiscoverer;
+import org.springframework.core.ParameterNameDiscoverer;
+import org.springframework.expression.Expression;
+import org.springframework.expression.spel.standard.SpelExpressionParser;
+import org.springframework.expression.spel.support.StandardEvaluationContext;
+
+import java.lang.reflect.Method;
+
+
+/**
+ * <p>
+ * 默认key 抽取, 优先根据 spel 处理
+ *
+ * @author 没秃顶的码农
+ * @date 2024-04-26
+ */
+public class ExpressionResolver implements KeyResolver {
+
+	private static final SpelExpressionParser PARSER = new SpelExpressionParser();
+
+	private static final ParameterNameDiscoverer DISCOVERER = new DefaultParameterNameDiscoverer();
+
+	@Override
+	public String resolver(Idempotent idempotent, JoinPoint point) {
+		Object[] arguments = point.getArgs();
+		String[] params = DISCOVERER.getParameterNames(getMethod(point));
+		StandardEvaluationContext context = new StandardEvaluationContext();
+
+		if (params != null && params.length > 0) {
+			for (int len = 0; len < params.length; len++) {
+				context.setVariable(params[len], arguments[len]);
+			}
+		}
+
+		Expression expression = PARSER.parseExpression(idempotent.key());
+		return expression.getValue(context, String.class);
+	}
+
+	/**
+	 * 根据切点解析方法信息
+	 * @param joinPoint 切点信息
+	 * @return Method 原信息
+	 */
+	private Method getMethod(JoinPoint joinPoint) {
+		MethodSignature signature = (MethodSignature) joinPoint.getSignature();
+		Method method = signature.getMethod();
+		if (method.getDeclaringClass().isInterface()) {
+			try {
+				method = joinPoint.getTarget().getClass().getDeclaredMethod(joinPoint.getSignature().getName(),
+						method.getParameterTypes());
+			}
+			catch (SecurityException | NoSuchMethodException e) {
+				throw new RuntimeException(e);
+			}
+		}
+		return method;
+	}
+
+}

+ 23 - 0
kxs-common/kxs-common-idempotent/src/main/java/com/kxs/common/idempotent/expression/KeyResolver.java

@@ -0,0 +1,23 @@
+package com.kxs.common.idempotent.expression;
+
+import com.kxs.common.idempotent.annotation.Idempotent;
+import org.aspectj.lang.JoinPoint;
+
+
+/**
+ * 唯一标志处理器
+ *
+ * @author 没秃顶的码农
+ * @date 2024-04-26
+ */
+public interface KeyResolver {
+
+	/**
+	 * 解析处理 key
+	 * @param idempotent 接口注解标识
+	 * @param point 接口切点信息
+	 * @return 处理结果
+	 */
+	String resolver(Idempotent idempotent, JoinPoint point);
+
+}

+ 1 - 0
kxs-common/kxs-common-idempotent/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

@@ -0,0 +1 @@
+com.kxs.common.idempotent.IdempotentAutoConfiguration

+ 1 - 0
kxs-common/pom.xml

@@ -25,6 +25,7 @@
         <module>kxs-common-seata</module>
         <module>kxs-common-seata</module>
         <module>kxs-common-oss</module>
         <module>kxs-common-oss</module>
         <module>kxs-common-mq</module>
         <module>kxs-common-mq</module>
+        <module>kxs-common-idempotent</module>
     </modules>
     </modules>
     <dependencyManagement>
     <dependencyManagement>
         <dependencies>
         <dependencies>

+ 4 - 0
kxs-product/kxs-product-biz/pom.xml

@@ -79,6 +79,10 @@
             <groupId>cn.hutool</groupId>
             <groupId>cn.hutool</groupId>
             <artifactId>hutool-json</artifactId>
             <artifactId>hutool-json</artifactId>
         </dependency>
         </dependency>
+        <dependency>
+            <groupId>com.kxs</groupId>
+            <artifactId>kxs-common-idempotent</artifactId>
+        </dependency>
     </dependencies>
     </dependencies>
 
 
     <build>
     <build>

+ 5 - 0
kxs-product/kxs-product-biz/src/main/java/com/kxs/product/biz/controller/admin/SysGdController.java

@@ -4,9 +4,11 @@ import cn.hutool.core.util.StrUtil;
 import com.baomidou.mybatisplus.core.toolkit.Wrappers;
 import com.baomidou.mybatisplus.core.toolkit.Wrappers;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.kxs.common.core.util.R;
 import com.kxs.common.core.util.R;
+import com.kxs.common.idempotent.annotation.Idempotent;
 import com.kxs.product.api.model.KxsGdReport;
 import com.kxs.product.api.model.KxsGdReport;
 import com.kxs.product.biz.service.KxsGdReportService;
 import com.kxs.product.biz.service.KxsGdReportService;
 import lombok.RequiredArgsConstructor;
 import lombok.RequiredArgsConstructor;
+import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.web.bind.annotation.*;
 import org.springframework.web.bind.annotation.*;
 
 
 /**
 /**
@@ -52,6 +54,7 @@ public class SysGdController {
      * @param param 实体对象
      * @param param 实体对象
      * @return 广电报备-编辑
      * @return 广电报备-编辑
      */
      */
+//    @PreAuthorize("@pms.hasPermission('sys_gd_report_edit')")
     @PutMapping("/update")
     @PutMapping("/update")
     public R update(@RequestBody KxsGdReport param) {
     public R update(@RequestBody KxsGdReport param) {
 
 
@@ -65,6 +68,8 @@ public class SysGdController {
      * @param endTime 结束时间
      * @param endTime 结束时间
      * @return 广电报备-编辑
      * @return 广电报备-编辑
      */
      */
+    @Idempotent(key = "#startTime", expireTime = 15, info = "请于15秒后重试")
+//    @PreAuthorize("@pms.hasPermission('sys_gd_report_check')")
     @GetMapping("/check")
     @GetMapping("/check")
     public R check(@RequestParam("startTime") String startTime, @RequestParam("endTime") String endTime) {
     public R check(@RequestParam("startTime") String startTime, @RequestParam("endTime") String endTime) {
 
 

+ 5 - 0
pom.xml

@@ -235,6 +235,11 @@
                 <artifactId>kxs-common-mybatis</artifactId>
                 <artifactId>kxs-common-mybatis</artifactId>
                 <version>${project.version}</version>
                 <version>${project.version}</version>
             </dependency>
             </dependency>
+            <dependency>
+                <groupId>com.kxs</groupId>
+                <artifactId>kxs-common-idempotent</artifactId>
+                <version>${project.version}</version>
+            </dependency>
             <dependency>
             <dependency>
                 <groupId>com.kxs</groupId>
                 <groupId>com.kxs</groupId>
                 <artifactId>kxs-common-seata</artifactId>
                 <artifactId>kxs-common-seata</artifactId>