diff --git a/mallchat-common/pom.xml b/mallchat-common/pom.xml index cb7d4ca..9f6b423 100644 --- a/mallchat-common/pom.xml +++ b/mallchat-common/pom.xml @@ -109,6 +109,11 @@ com.github.ben-manes.caffeine caffeine + + org.redisson + redisson-spring-boot-starter + 3.17.1 + \ No newline at end of file diff --git a/mallchat-common/src/main/java/com/abin/mallchat/common/common/annotation/RedissonLock.java b/mallchat-common/src/main/java/com/abin/mallchat/common/common/annotation/RedissonLock.java new file mode 100644 index 0000000..eeb7a93 --- /dev/null +++ b/mallchat-common/src/main/java/com/abin/mallchat/common/common/annotation/RedissonLock.java @@ -0,0 +1,40 @@ +package com.abin.mallchat.common.common.annotation; + +import java.lang.annotation.*; +import java.util.concurrent.TimeUnit; + +/** + * 分布式锁注解 + */ +@Retention(RetentionPolicy.RUNTIME)//运行时生效 +@Target(ElementType.METHOD)//作用在方法上 +public @interface RedissonLock { + /** + * key的前缀,默认取方法全限定名,除非我们在不同方法上对同一个资源做分布式锁,就自己指定 + * + * @return key的前缀 + */ + String prefixKey() default ""; + + /** + * springEl 表达式 + * + * @return 表达式 + */ + String key(); + + /** + * 等待锁的时间,默认-1,不等待直接失败,redisson默认也是-1 + * + * @return 单位秒 + */ + int waitTime() default -1; + + /** + * 等待锁的时间单位,默认毫秒 + * + * @return 单位 + */ + TimeUnit unit() default TimeUnit.MILLISECONDS; + +} diff --git a/mallchat-common/src/main/java/com/abin/mallchat/common/common/aspect/FrequencyControlAspect.java b/mallchat-common/src/main/java/com/abin/mallchat/common/common/aspect/FrequencyControlAspect.java index 3347dc6..58fc660 100644 --- a/mallchat-common/src/main/java/com/abin/mallchat/common/common/aspect/FrequencyControlAspect.java +++ b/mallchat-common/src/main/java/com/abin/mallchat/common/common/aspect/FrequencyControlAspect.java @@ -6,6 +6,7 @@ import com.abin.mallchat.common.common.exception.BusinessException; import com.abin.mallchat.common.common.exception.CommonErrorEnum; import com.abin.mallchat.common.common.utils.RedisUtils; import com.abin.mallchat.common.common.utils.RequestHolder; +import com.abin.mallchat.common.common.utils.SpElUtils; import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; @@ -33,8 +34,6 @@ import java.util.*; public class FrequencyControlAspect { @Autowired private RedisUtils redisUtils; - private final ExpressionParser parser = new SpelExpressionParser(); - private final DefaultParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer(); @Around("@annotation(com.abin.mallchat.common.common.annotation.FrequencyControl)||@annotation(com.abin.mallchat.common.common.annotation.FrequencyControlContainer)") public Object around(ProceedingJoinPoint joinPoint) throws Throwable { @@ -43,11 +42,11 @@ public class FrequencyControlAspect { Map keyMap = new HashMap<>(); for (int i = 0; i < annotationsByType.length; i++) { FrequencyControl frequencyControl = annotationsByType[i]; - String prefix = StrUtil.isBlank(frequencyControl.prefixKey()) ? method.toGenericString() + ":index:" + i : frequencyControl.prefixKey();//默认方法限定名+注解排名(可能多个) + String prefix = StrUtil.isBlank(frequencyControl.prefixKey()) ? SpElUtils.getMethodKey(method) + ":index:" + i : frequencyControl.prefixKey();//默认方法限定名+注解排名(可能多个) String key = ""; switch (frequencyControl.target()) { case EL: - key = parseSpEl(method, joinPoint.getArgs(), frequencyControl.spEl()); + key = SpElUtils.parseSpEl(method, joinPoint.getArgs(), frequencyControl.spEl()); break; case IP: key = RequestHolder.get().getIp(); @@ -79,13 +78,5 @@ public class FrequencyControlAspect { } } - private String parseSpEl(Method method, Object[] args, String spEl) { - String[] params = parameterNameDiscoverer.getParameterNames(method);//解析参数名 - EvaluationContext context = new StandardEvaluationContext();//el解析需要的上下文对象 - for (int i = 0; i < params.length; i++) { - context.setVariable(params[i], args[i]);//所有参数都作为原材料扔进去 - } - Expression expression = parser.parseExpression(spEl); - return expression.getValue(context, String.class); - } + } diff --git a/mallchat-common/src/main/java/com/abin/mallchat/common/common/aspect/RedissonLockAspect.java b/mallchat-common/src/main/java/com/abin/mallchat/common/common/aspect/RedissonLockAspect.java new file mode 100644 index 0000000..4c5b9c4 --- /dev/null +++ b/mallchat-common/src/main/java/com/abin/mallchat/common/common/aspect/RedissonLockAspect.java @@ -0,0 +1,50 @@ +package com.abin.mallchat.common.common.aspect; + +import cn.hutool.core.util.StrUtil; +import com.abin.mallchat.common.common.annotation.FrequencyControl; +import com.abin.mallchat.common.common.annotation.RedissonLock; +import com.abin.mallchat.common.common.exception.BusinessException; +import com.abin.mallchat.common.common.exception.CommonErrorEnum; +import com.abin.mallchat.common.common.service.LockService; +import com.abin.mallchat.common.common.utils.RedisUtils; +import com.abin.mallchat.common.common.utils.RequestHolder; +import com.abin.mallchat.common.common.utils.SpElUtils; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.reflect.MethodSignature; +import org.redisson.api.RedissonClient; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.DefaultParameterNameDiscoverer; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.Expression; +import org.springframework.expression.ExpressionParser; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.expression.spel.support.StandardEvaluationContext; +import org.springframework.stereotype.Component; + +import java.lang.reflect.Method; +import java.util.*; + +/** + * Description: 分布式锁切面 + * Author: abin + * Date: 2023-04-20 + */ +@Slf4j +@Aspect +@Component +public class RedissonLockAspect { + @Autowired + private LockService lockService; + + @Around("@annotation(com.abin.mallchat.common.common.annotation.RedissonLock)") + public Object around(ProceedingJoinPoint joinPoint) throws Throwable { + Method method = ((MethodSignature) joinPoint.getSignature()).getMethod(); + RedissonLock redissonLock = method.getAnnotation(RedissonLock.class); + String prefix = StrUtil.isBlank(redissonLock.prefixKey()) ? SpElUtils.getMethodKey(method) : redissonLock.prefixKey();//默认方法限定名+注解排名(可能多个) + String key = SpElUtils.parseSpEl(method, joinPoint.getArgs(), redissonLock.key()); + return lockService.executeWithLockThrows(prefix + ":" + key, redissonLock.waitTime(), redissonLock.unit(), joinPoint::proceed); + } +} diff --git a/mallchat-common/src/main/java/com/abin/mallchat/common/common/config/RedissonConfig.java b/mallchat-common/src/main/java/com/abin/mallchat/common/common/config/RedissonConfig.java new file mode 100644 index 0000000..da33172 --- /dev/null +++ b/mallchat-common/src/main/java/com/abin/mallchat/common/common/config/RedissonConfig.java @@ -0,0 +1,30 @@ +package com.abin.mallchat.common.common.config; + +import org.redisson.Redisson; +import org.redisson.api.RedissonClient; +import org.redisson.config.Config; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.data.redis.RedisProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * Description: + * Author: abin + * Date: 2023-04-22 + */ +@Configuration +public class RedissonConfig { + @Autowired + private RedisProperties redisProperties; + + @Bean + public RedissonClient redissonClient() { + Config config = new Config(); + config.useSingleServer() + .setAddress("redis://" + redisProperties.getHost() + ":" + redisProperties.getPort()) + .setPassword(redisProperties.getPassword()) + .setDatabase(redisProperties.getDatabase()); + return Redisson.create(config); + } +} diff --git a/mallchat-common/src/main/java/com/abin/mallchat/common/common/exception/CommonErrorEnum.java b/mallchat-common/src/main/java/com/abin/mallchat/common/common/exception/CommonErrorEnum.java index f023de8..10804e8 100644 --- a/mallchat-common/src/main/java/com/abin/mallchat/common/common/exception/CommonErrorEnum.java +++ b/mallchat-common/src/main/java/com/abin/mallchat/common/common/exception/CommonErrorEnum.java @@ -15,6 +15,7 @@ public enum CommonErrorEnum implements ErrorEnum { SYSTEM_ERROR(-1, "系统出小差了,请稍后再试哦~~"), PARAM_VALID(-2, "参数校验失败"), FREQUENCY_LIMIT(-3, "请求太频繁了,请稍后再试哦~~"), + LOCK_LIMIT(-4, "请求太频繁了,请稍后再试哦~~"), ; private Integer code; private String msg; diff --git a/mallchat-common/src/main/java/com/abin/mallchat/common/common/service/LockService.java b/mallchat-common/src/main/java/com/abin/mallchat/common/common/service/LockService.java new file mode 100644 index 0000000..98c8ffe --- /dev/null +++ b/mallchat-common/src/main/java/com/abin/mallchat/common/common/service/LockService.java @@ -0,0 +1,51 @@ +package com.abin.mallchat.common.common.service; + +import com.abin.mallchat.common.common.exception.BusinessException; +import com.abin.mallchat.common.common.exception.CommonErrorEnum; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.redisson.api.RLock; +import org.redisson.api.RedissonClient; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.concurrent.Callable; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; + +@Service +@Slf4j +public class LockService { + + @Autowired + private RedissonClient redissonClient; + + public T executeWithLockThrows(String key, int waitTime, TimeUnit unit, SupplierThrow supplier) throws Throwable { + RLock lock = redissonClient.getLock(key); + boolean lockSuccess = lock.tryLock(waitTime, unit); + if (!lockSuccess) { + throw new BusinessException(CommonErrorEnum.LOCK_LIMIT); + } + try { + return supplier.get();//执行锁内的代码逻辑 + } finally { + lock.unlock(); + } + } + + @SneakyThrows + public T executeWithLock(String key, int waitTime, TimeUnit unit, Supplier supplier) { + return executeWithLockThrows(key, waitTime, unit, supplier::get); + } + + @FunctionalInterface + public interface SupplierThrow { + + /** + * Gets a result. + * + * @return a result + */ + T get() throws Throwable; + } +} \ No newline at end of file diff --git a/mallchat-common/src/main/java/com/abin/mallchat/common/common/utils/SpElUtils.java b/mallchat-common/src/main/java/com/abin/mallchat/common/common/utils/SpElUtils.java new file mode 100644 index 0000000..79c5c8c --- /dev/null +++ b/mallchat-common/src/main/java/com/abin/mallchat/common/common/utils/SpElUtils.java @@ -0,0 +1,35 @@ +package com.abin.mallchat.common.common.utils; + +import org.springframework.core.DefaultParameterNameDiscoverer; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.Expression; +import org.springframework.expression.ExpressionParser; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.expression.spel.support.StandardEvaluationContext; +import org.springframework.stereotype.Component; + +import java.lang.reflect.Method; + +/** + * Description: spring el表达式解析 + * Author: abin + * Date: 2023-04-22 + */ +public class SpElUtils { + private static final ExpressionParser parser = new SpelExpressionParser(); + private static final DefaultParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer(); + + public static String parseSpEl(Method method, Object[] args, String spEl) { + String[] params = parameterNameDiscoverer.getParameterNames(method);//解析参数名 + EvaluationContext context = new StandardEvaluationContext();//el解析需要的上下文对象 + for (int i = 0; i < params.length; i++) { + context.setVariable(params[i], args[i]);//所有参数都作为原材料扔进去 + } + Expression expression = parser.parseExpression(spEl); + return expression.getValue(context, String.class); + } + + public static String getMethodKey(Method method){ + return method.getDeclaringClass()+"#"+method.getName(); + } +} diff --git a/mallchat-common/src/main/java/com/abin/mallchat/common/user/service/impl/UserBackpackServiceImpl.java b/mallchat-common/src/main/java/com/abin/mallchat/common/user/service/impl/UserBackpackServiceImpl.java index 01daeca..d702329 100644 --- a/mallchat-common/src/main/java/com/abin/mallchat/common/user/service/impl/UserBackpackServiceImpl.java +++ b/mallchat-common/src/main/java/com/abin/mallchat/common/user/service/impl/UserBackpackServiceImpl.java @@ -1,5 +1,6 @@ package com.abin.mallchat.common.user.service.impl; +import com.abin.mallchat.common.common.annotation.RedissonLock; import com.abin.mallchat.common.common.domain.enums.IdempotentEnum; import com.abin.mallchat.common.common.domain.enums.YesOrNoEnum; import com.abin.mallchat.common.user.dao.ItemConfigDao; @@ -32,6 +33,7 @@ public class UserBackpackServiceImpl implements IUserBackpackService { private ItemCache itemCache; @Override + @RedissonLock(key = "#uid") public void acquireItem(Long uid, Long itemId, IdempotentEnum idempotentEnum, String businessId) {//todo 分布式锁 String idempotent = getIdempotent(itemId, idempotentEnum, businessId); UserBackpack userBackpack = userBackpackDao.getByIdp(idempotent); diff --git a/mallchat-custom-server/src/main/java/com/abin/mallchat/custom/common/config/SwaggerConfig.java b/mallchat-custom-server/src/main/java/com/abin/mallchat/custom/common/config/SwaggerConfig.java index 796657e..4446156 100644 --- a/mallchat-custom-server/src/main/java/com/abin/mallchat/custom/common/config/SwaggerConfig.java +++ b/mallchat-custom-server/src/main/java/com/abin/mallchat/custom/common/config/SwaggerConfig.java @@ -1,10 +1,19 @@ package com.abin.mallchat.custom.common.config; +import org.springframework.boot.actuate.autoconfigure.endpoint.web.CorsEndpointProperties; +import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties; +import org.springframework.boot.actuate.autoconfigure.web.server.ManagementPortType; +import org.springframework.boot.actuate.endpoint.ExposableEndpoint; +import org.springframework.boot.actuate.endpoint.web.*; +import org.springframework.boot.actuate.endpoint.web.annotation.ControllerEndpointsSupplier; +import org.springframework.boot.actuate.endpoint.web.annotation.ServletEndpointsSupplier; +import org.springframework.boot.actuate.endpoint.web.servlet.WebMvcEndpointHandlerMapping; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.core.env.Environment; import org.springframework.core.env.Profiles; +import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.servlet.config.annotation.EnableWebMvc; import org.springframework.web.servlet.config.annotation.ViewControllerRegistry; @@ -17,6 +26,10 @@ import springfox.documentation.spi.DocumentationType; import springfox.documentation.spring.web.plugins.Docket; import springfox.documentation.swagger2.annotations.EnableSwagger2WebMvc; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + /** * Description: * Author: abin @@ -47,21 +60,22 @@ public class SwaggerConfig { .build(); } /** - * swagger 配置 - * @param environment 环境 - */ -// @Bean -// public Docket docket(Environment environment) { -// -// // 设置环境范围 -// Profiles profiles = Profiles.of("dev","test"); -// // 如果在该环境返回内则返回:true,反之返回 false -// boolean flag = environment.acceptsProfiles(profiles); -// -// // 创建一个 swagger 的 bean 实例 -// return new Docket(DocumentationType.SWAGGER_2) -// .enable(flag) // 是否开启 swagger:true -> 开启,false -> 关闭 -// ; -// } + * 增加如下配置可解决Spring Boot 6.x 与Swagger 3.0.0 不兼容问题 + **/ + @Bean + public WebMvcEndpointHandlerMapping webEndpointServletHandlerMapping(WebEndpointsSupplier webEndpointsSupplier, ServletEndpointsSupplier servletEndpointsSupplier, ControllerEndpointsSupplier controllerEndpointsSupplier, EndpointMediaTypes endpointMediaTypes, CorsEndpointProperties corsProperties, WebEndpointProperties webEndpointProperties, Environment environment) { + List> allEndpoints = new ArrayList(); + Collection webEndpoints = webEndpointsSupplier.getEndpoints(); + allEndpoints.addAll(webEndpoints); + allEndpoints.addAll(servletEndpointsSupplier.getEndpoints()); + allEndpoints.addAll(controllerEndpointsSupplier.getEndpoints()); + String basePath = webEndpointProperties.getBasePath(); + EndpointMapping endpointMapping = new EndpointMapping(basePath); + boolean shouldRegisterLinksMapping = this.shouldRegisterLinksMapping(webEndpointProperties, environment, basePath); + return new WebMvcEndpointHandlerMapping(endpointMapping, webEndpoints, endpointMediaTypes, corsProperties.toCorsConfiguration(), new EndpointLinksResolver(allEndpoints, basePath), shouldRegisterLinksMapping, null); + } + private boolean shouldRegisterLinksMapping(WebEndpointProperties webEndpointProperties, Environment environment, String basePath) { + return webEndpointProperties.getDiscovery().isEnabled() && (StringUtils.hasText(basePath) || ManagementPortType.get(environment).equals(ManagementPortType.DIFFERENT)); + } }