This commit is contained in:
kk
2026-07-07 18:01:21 +08:00
commit b259f94d4b
1088 changed files with 121778 additions and 0 deletions
+59
View File
@@ -0,0 +1,59 @@
<?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>
<artifactId>BladeX-Tool</artifactId>
<groupId>org.springblade</groupId>
<version>${revision}</version>
</parent>
<artifactId>blade-starter-redis</artifactId>
<name>${project.artifactId}</name>
<version>${project.parent.version}</version>
<packaging>jar</packaging>
<properties>
<module.name>org.springblade.blade.starter.redis</module.name>
</properties>
<dependencies>
<!--Blade-->
<dependency>
<groupId>org.springblade</groupId>
<artifactId>blade-core-tool</artifactId>
</dependency>
<dependency>
<groupId>org.springblade</groupId>
<artifactId>blade-starter-auth</artifactId>
</dependency>
<!--Redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
</dependency>
<!-- protostuff -->
<dependency>
<groupId>io.protostuff</groupId>
<artifactId>protostuff-core</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>io.protostuff</groupId>
<artifactId>protostuff-runtime</artifactId>
<optional>true</optional>
</dependency>
<!-- Auto -->
<dependency>
<groupId>org.springblade</groupId>
<artifactId>blade-core-auto</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
</project>
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,59 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: DreamLu (596392912@qq.com)
*/
package org.springblade.core.redis.cache;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.ToString;
import org.springframework.lang.Nullable;
import java.time.Duration;
/**
* cache key 封装
*
* @author L.cm
*/
@Getter
@ToString
@AllArgsConstructor
public class CacheKey {
/**
* redis key
*/
private final String key;
/**
* 超时时间 秒
*/
@Nullable
private Duration expire;
public CacheKey(String key) {
this.key = key;
}
}
@@ -0,0 +1,80 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: DreamLu (596392912@qq.com)
*/
package org.springblade.core.redis.cache;
import org.springblade.core.tool.utils.ObjectUtil;
import org.springblade.core.tool.utils.StringPool;
import org.springblade.core.tool.utils.StringUtil;
import org.springframework.lang.Nullable;
import java.time.Duration;
/**
* cache key
*
* @author L.cm
*/
public interface ICacheKey {
/**
* 获取前缀
*
* @return key 前缀
*/
String getPrefix();
/**
* 超时时间
*
* @return 超时时间
*/
@Nullable
default Duration getExpire() {
return null;
}
/**
* 组装 cache key
*
* @param suffix 参数
* @return cache key
*/
default CacheKey getKey(Object... suffix) {
String prefix = this.getPrefix();
// 拼接参数
String key;
if (ObjectUtil.isEmpty(suffix)) {
key = prefix;
} else {
key = prefix.concat(StringUtil.join(suffix, StringPool.COLON));
}
Duration expire = this.getExpire();
return expire == null ? new CacheKey(key) : new CacheKey(key, expire);
}
}
@@ -0,0 +1,124 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: DreamLu (596392912@qq.com)
*/
package org.springblade.core.redis.config;
import org.springblade.core.jwt.config.JwtRedisConfiguration;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.cache.CacheManagerCustomizers;
import org.springframework.boot.autoconfigure.cache.CacheProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;
import org.springframework.data.redis.cache.BatchStrategies;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.cache.RedisCacheWriter;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.lang.Nullable;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* 扩展redis-cache支持注解cacheName添加超时时间
* <p>
*
* @author L.cm
*/
@AutoConfiguration(before = JwtRedisConfiguration.class)
@EnableConfigurationProperties(CacheProperties.class)
public class BladeRedisCacheAutoConfiguration {
/**
* 序列化方式
*/
private final RedisSerializer<Object> redisSerializer;
private final CacheProperties cacheProperties;
private final CacheManagerCustomizers customizerInvoker;
@Nullable
private final RedisCacheConfiguration redisCacheConfiguration;
BladeRedisCacheAutoConfiguration(RedisSerializer<Object> redisSerializer,
CacheProperties cacheProperties,
CacheManagerCustomizers customizerInvoker,
ObjectProvider<RedisCacheConfiguration> redisCacheConfiguration) {
this.redisSerializer = redisSerializer;
this.cacheProperties = cacheProperties;
this.customizerInvoker = customizerInvoker;
this.redisCacheConfiguration = redisCacheConfiguration.getIfAvailable();
}
@Primary
@Bean("redisCacheManager")
public RedisCacheManager redisCacheManager(RedisConnectionFactory connectionFactory) {
RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory, BatchStrategies.scan(1000));
RedisCacheConfiguration cacheConfiguration = this.determineConfiguration();
List<String> cacheNames = this.cacheProperties.getCacheNames();
Map<String, RedisCacheConfiguration> initialCaches = new LinkedHashMap<>();
if (!cacheNames.isEmpty()) {
Map<String, RedisCacheConfiguration> cacheConfigMap = new LinkedHashMap<>(cacheNames.size());
cacheNames.forEach(it -> cacheConfigMap.put(it, cacheConfiguration));
initialCaches.putAll(cacheConfigMap);
}
boolean allowInFlightCacheCreation = true;
boolean enableTransactions = false;
RedisAutoCacheManager cacheManager = new RedisAutoCacheManager(redisCacheWriter, cacheConfiguration, initialCaches, allowInFlightCacheCreation);
cacheManager.setTransactionAware(enableTransactions);
return this.customizerInvoker.customize(cacheManager);
}
private RedisCacheConfiguration determineConfiguration() {
if (this.redisCacheConfiguration != null) {
return this.redisCacheConfiguration;
} else {
CacheProperties.Redis redisProperties = this.cacheProperties.getRedis();
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
config = config.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer));
if (redisProperties.getTimeToLive() != null) {
config = config.entryTtl(redisProperties.getTimeToLive());
}
if (redisProperties.getKeyPrefix() != null) {
config = config.prefixCacheNameWith(redisProperties.getKeyPrefix());
}
if (!redisProperties.isCacheNullValues()) {
config = config.disableCachingNullValues();
}
if (!redisProperties.isUseKeyPrefix()) {
config = config.disableKeyPrefix();
}
return config;
}
}
}
@@ -0,0 +1,95 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: DreamLu (596392912@qq.com)
*/
package org.springblade.core.redis.config;
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import java.time.Duration;
/**
* redis 配置
*
* @author L.cm
*/
@Getter
@Setter
@ConfigurationProperties(BladeRedisProperties.PREFIX)
public class BladeRedisProperties {
public static final String PREFIX = "blade.redis";
/**
* 序列化方式
*/
private SerializerType serializerType = SerializerType.ProtoStuff;
/**
* stream
*/
private Stream stream = new Stream();
public enum SerializerType {
/**
* 默认:ProtoStuff 序列化
*/
ProtoStuff,
/**
* json 序列化
*/
JSON,
/**
* jdk 序列化
*/
JDK
}
@Getter
@Setter
public static class Stream {
public static final String PREFIX = BladeRedisProperties.PREFIX + ".stream";
/**
* 是否开启 stream
*/
boolean enable = false;
/**
* consumer group,默认:服务名 + 环境
*/
String consumerGroup;
/**
* 消费者名称,默认:ip + 端口
*/
String consumerName;
/**
* poll 批量大小
*/
Integer pollBatchSize;
/**
* poll 超时时间
*/
Duration pollTimeout;
}
}
@@ -0,0 +1,75 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: DreamLu (596392912@qq.com)
*/
package org.springblade.core.redis.config;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
/**
* redis 序列化
*
* @author L.cm
*/
public interface BladeRedisSerializerConfigAble {
/**
* JSON序列化类型字段
*/
String TYPE_NAME = "@class";
/**
* 序列化接口
*
* @param properties 配置
* @return RedisSerializer
*/
RedisSerializer<Object> redisSerializer(BladeRedisProperties properties);
/**
* 默认的序列化方式
*
* @param properties 配置
* @return RedisSerializer
*/
default RedisSerializer<Object> defaultRedisSerializer(BladeRedisProperties properties) {
BladeRedisProperties.SerializerType serializerType = properties.getSerializerType();
if (BladeRedisProperties.SerializerType.JDK == serializerType) {
/**
* SpringBoot扩展了ClassLoader,进行分离打包的时候,使用到JdkSerializationRedisSerializer的地方
* 会因为ClassLoader的不同导致加载不到Class
* 指定使用项目的ClassLoader
*
* JdkSerializationRedisSerializer默认使用{@link sun.misc.Launcher.AppClassLoader}
* SpringBoot默认使用{@link org.springframework.boot.loader.LaunchedURLClassLoader}
*/
ClassLoader classLoader = this.getClass().getClassLoader();
return new JdkSerializationRedisSerializer(classLoader);
}
return new GenericJackson2JsonRedisSerializer(TYPE_NAME);
}
}
@@ -0,0 +1,55 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: DreamLu (596392912@qq.com)
*/
package org.springblade.core.redis.config;
import org.springblade.core.redis.serializer.ProtoStuffSerializer;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.data.redis.serializer.RedisSerializer;
/**
* ProtoStuff 序列化配置
*
* @author L.cm
*/
@AutoConfiguration(before = RedisTemplateConfiguration.class)
@ConditionalOnClass(name = "io.protostuff.Schema")
public class ProtoStuffSerializerConfiguration implements BladeRedisSerializerConfigAble {
@Bean
@ConditionalOnMissingBean
@Override
public RedisSerializer<Object> redisSerializer(BladeRedisProperties properties) {
if (BladeRedisProperties.SerializerType.ProtoStuff == properties.getSerializerType()) {
return new ProtoStuffSerializer();
}
return defaultRedisSerializer(properties);
}
}
@@ -0,0 +1,73 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: DreamLu (596392912@qq.com)
*/
package org.springblade.core.redis.config;
import org.springblade.core.redis.ratelimiter.RedisRateLimiterAspect;
import org.springblade.core.redis.ratelimiter.RedisRateLimiterClient;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.core.env.Environment;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.scripting.support.ResourceScriptSource;
import java.util.List;
/**
* 基于 redis 的分布式限流自动配置
*
* @author L.cm
*/
@AutoConfiguration
@ConditionalOnProperty(value = "blade.redis.rate-limiter.enabled", havingValue = "true")
public class RateLimiterAutoConfiguration {
private RedisScript<Long> redisRateLimiterScript() {
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("META-INF/scripts/blade_rate_limiter.lua")));
redisScript.setResultType(Long.class);
return redisScript;
}
@Bean
@ConditionalOnMissingBean
public RedisRateLimiterClient redisRateLimiter(StringRedisTemplate redisTemplate,
Environment environment) {
RedisScript<Long> redisRateLimiterScript = redisRateLimiterScript();
return new RedisRateLimiterClient(redisTemplate, redisRateLimiterScript, environment);
}
@Bean
@ConditionalOnMissingBean
public RedisRateLimiterAspect redisRateLimiterAspect(RedisRateLimiterClient rateLimiterClient) {
return new RedisRateLimiterAspect(rateLimiterClient);
}
}
@@ -0,0 +1,73 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: DreamLu (596392912@qq.com)
*/
package org.springblade.core.redis.config;
import org.springblade.core.tool.utils.StringPool;
import org.springblade.core.tool.utils.StringUtil;
import org.springframework.boot.convert.DurationStyle;
import org.springframework.data.redis.cache.RedisCache;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.cache.RedisCacheWriter;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import java.time.Duration;
import java.time.temporal.ChronoUnit;
import java.util.Map;
/**
* redis cache 扩展cache name自动化配置
*
* @author L.cm
*/
public class RedisAutoCacheManager extends RedisCacheManager {
public RedisAutoCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration,
Map<String, RedisCacheConfiguration> initialCacheConfigurations, boolean allowInFlightCacheCreation) {
super(cacheWriter, defaultCacheConfiguration, allowInFlightCacheCreation, initialCacheConfigurations);
}
@NonNull
@Override
protected RedisCache createRedisCache(@NonNull String name, @Nullable RedisCacheConfiguration cacheConfig) {
if (StringUtil.isBlank(name) || !name.contains(StringPool.HASH)) {
return super.createRedisCache(name, cacheConfig);
}
String[] cacheArray = name.split(StringPool.HASH);
if (cacheArray.length < 2) {
return super.createRedisCache(name, cacheConfig);
}
String cacheName = cacheArray[0];
if (cacheConfig != null) {
Duration cacheAge = DurationStyle.detectAndParse(cacheArray[1], ChronoUnit.SECONDS);;
cacheConfig = cacheConfig.entryTtl(cacheAge);
}
return super.createRedisCache(cacheName, cacheConfig);
}
}
@@ -0,0 +1,52 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: DreamLu (596392912@qq.com)
*/
package org.springblade.core.redis.config;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.cache.CacheManagerCustomizer;
import org.springframework.boot.autoconfigure.cache.CacheManagerCustomizers;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import java.util.List;
/**
* CacheManagerCustomizers配置
*
* @author L.cm
*/
@AutoConfiguration
@ConditionalOnMissingBean(CacheManagerCustomizers.class)
public class RedisCacheManagerConfig {
@Bean
public CacheManagerCustomizers cacheManagerCustomizers(
ObjectProvider<List<CacheManagerCustomizer<?>>> customizers) {
return new CacheManagerCustomizers(customizers.getIfAvailable());
}
}
@@ -0,0 +1,77 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: DreamLu (596392912@qq.com)
*/
package org.springblade.core.redis.config;
import org.springblade.core.redis.cache.BladeRedis;
import org.springblade.core.redis.pubsub.RPubSubListenerDetector;
import org.springblade.core.redis.pubsub.RPubSubListenerLazyFilter;
import org.springblade.core.redis.pubsub.RPubSubPublisher;
import org.springblade.core.redis.pubsub.RedisPubSubPublisher;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.serializer.RedisSerializer;
/**
* Redisson pub/sub 发布配置
*
* @author L.cm
*/
@AutoConfiguration
public class RedisPubSubConfiguration {
@Bean
@ConditionalOnMissingBean
public RedisMessageListenerContainer redisMessageListenerContainer(RedisConnectionFactory connectionFactory) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
return container;
}
@Bean
public RPubSubPublisher topicEventPublisher(BladeRedis bladeRedis,
RedisSerializer<Object> redisSerializer) {
return new RedisPubSubPublisher(bladeRedis, redisSerializer);
}
@Bean
@ConditionalOnBean(RedisSerializer.class)
public RPubSubListenerDetector topicListenerDetector(RedisMessageListenerContainer redisMessageListenerContainer,
RedisSerializer<Object> redisSerializer) {
return new RPubSubListenerDetector(redisMessageListenerContainer, redisSerializer);
}
@Bean
public RPubSubListenerLazyFilter rPubSubListenerLazyFilter() {
return new RPubSubListenerLazyFilter();
}
}
@@ -0,0 +1,140 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: DreamLu (596392912@qq.com)
*/
package org.springblade.core.redis.config;
import org.springblade.core.launch.utils.INetUtil;
import org.springblade.core.redis.stream.DefaultRStreamTemplate;
import org.springblade.core.redis.stream.RStreamListenerDetector;
import org.springblade.core.redis.stream.RStreamTemplate;
import org.springblade.core.tool.utils.CharPool;
import org.springblade.core.tool.utils.StringUtil;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.core.env.Environment;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.stream.MapRecord;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.stream.StreamMessageListenerContainer;
import org.springframework.data.redis.stream.StreamMessageListenerContainer.StreamMessageListenerContainerOptions;
import org.springframework.util.ErrorHandler;
import java.time.Duration;
/**
* redis Stream 配置
*
* @author L.cm
*/
@AutoConfiguration
@ConditionalOnProperty(
prefix = BladeRedisProperties.Stream.PREFIX,
name = "enable",
havingValue = "true"
)
public class RedisStreamConfiguration {
/**
* Spring 应用名 prop key
*/
private static final String SPRING_APP_NAME_KEY = "spring.application.name";
/**
* The "active profiles" property name.
*/
private static final String ACTIVE_PROFILES_PROPERTY = "spring.profiles.active";
@Bean
@ConditionalOnMissingBean
public StreamMessageListenerContainerOptions<String, MapRecord<String, String, byte[]>> streamMessageListenerContainerOptions(BladeRedisProperties properties,
ObjectProvider<ErrorHandler> errorHandlerObjectProvider) {
StreamMessageListenerContainer.StreamMessageListenerContainerOptionsBuilder<String, MapRecord<String, String, byte[]>> builder = StreamMessageListenerContainerOptions
.builder()
.keySerializer(RedisSerializer.string())
.hashKeySerializer(RedisSerializer.string())
.hashValueSerializer(RedisSerializer.byteArray());
BladeRedisProperties.Stream streamProperties = properties.getStream();
// 批量大小
Integer pollBatchSize = streamProperties.getPollBatchSize();
if (pollBatchSize != null && pollBatchSize > 0) {
builder.batchSize(pollBatchSize);
}
// poll 超时时间
Duration pollTimeout = streamProperties.getPollTimeout();
if (pollTimeout != null && !pollTimeout.isNegative()) {
builder.pollTimeout(pollTimeout);
}
// errorHandler
errorHandlerObjectProvider.ifAvailable((builder::errorHandler));
// TODO L.cm executor
return builder.build();
}
@Bean
@ConditionalOnMissingBean
public StreamMessageListenerContainer<String, MapRecord<String, String, byte[]>> streamMessageListenerContainer(RedisConnectionFactory redisConnectionFactory,
StreamMessageListenerContainerOptions<String, MapRecord<String, String, byte[]>> streamMessageListenerContainerOptions) {
// 根据配置对象创建监听容器
return StreamMessageListenerContainer.create(redisConnectionFactory, streamMessageListenerContainerOptions);
}
@Bean
@ConditionalOnMissingBean
public RStreamListenerDetector streamListenerDetector(StreamMessageListenerContainer<String, MapRecord<String, String, byte[]>> streamMessageListenerContainer,
RedisTemplate<String, Object> redisTemplate,
ObjectProvider<ServerProperties> serverPropertiesObjectProvider,
BladeRedisProperties properties,
Environment environment) {
BladeRedisProperties.Stream streamProperties = properties.getStream();
// 消费组名称
String consumerGroup = streamProperties.getConsumerGroup();
if (StringUtil.isBlank(consumerGroup)) {
String appName = environment.getRequiredProperty(SPRING_APP_NAME_KEY);
String profile = environment.getProperty(ACTIVE_PROFILES_PROPERTY);
consumerGroup = StringUtil.isBlank(profile) ? appName : appName + CharPool.COLON + profile;
}
// 消费者名称
String consumerName = streamProperties.getConsumerName();
if (StringUtil.isBlank(consumerName)) {
final StringBuilder consumerNameBuilder = new StringBuilder(INetUtil.getHostIp());
serverPropertiesObjectProvider.ifAvailable(serverProperties -> {
consumerNameBuilder.append(CharPool.COLON).append(serverProperties.getPort());
});
consumerName = consumerNameBuilder.toString();
}
return new RStreamListenerDetector(streamMessageListenerContainer, redisTemplate, consumerGroup, consumerName);
}
@Bean
public RStreamTemplate streamTemplate(RedisTemplate<String, Object> redisTemplate) {
return new DefaultRStreamTemplate(redisTemplate);
}
}
@@ -0,0 +1,96 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: DreamLu (596392912@qq.com)
*/
package org.springblade.core.redis.config;
import org.springblade.core.jwt.config.JwtRedisConfiguration;
import org.springblade.core.redis.cache.BladeRedis;
import org.springblade.core.redis.serializer.ProtoStuffSerializer;
import org.springblade.core.redis.serializer.RedisKeySerializer;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.data.redis.serializer.RedisSerializer;
/**
* RedisTemplate 配置
*
* @author L.cm
*/
@EnableCaching
@AutoConfiguration(before = {JwtRedisConfiguration.class, RedisAutoConfiguration.class})
@EnableConfigurationProperties(BladeRedisProperties.class)
public class RedisTemplateConfiguration implements BladeRedisSerializerConfigAble {
/**
* value 值 序列化
*
* @return RedisSerializer
*/
@Bean
@ConditionalOnMissingBean(RedisSerializer.class)
@Override
public RedisSerializer<Object> redisSerializer(BladeRedisProperties properties) {
if (BladeRedisProperties.SerializerType.ProtoStuff == properties.getSerializerType()) {
return new ProtoStuffSerializer();
}
return defaultRedisSerializer(properties);
}
@Bean(name = "redisTemplate")
@ConditionalOnMissingBean(name = "redisTemplate")
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory, RedisSerializer<Object> redisSerializer) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
// key 序列化
RedisKeySerializer keySerializer = new RedisKeySerializer();
redisTemplate.setKeySerializer(keySerializer);
redisTemplate.setHashKeySerializer(keySerializer);
// value 序列化
redisTemplate.setValueSerializer(redisSerializer);
redisTemplate.setHashValueSerializer(redisSerializer);
redisTemplate.setConnectionFactory(redisConnectionFactory);
return redisTemplate;
}
@Bean
@ConditionalOnMissingBean(ValueOperations.class)
public ValueOperations valueOperations(RedisTemplate redisTemplate) {
return redisTemplate.opsForValue();
}
@Bean
public BladeRedis bladeRedis(RedisTemplate<String, Object> redisTemplate, StringRedisTemplate stringRedisTemplate) {
return new BladeRedis(redisTemplate, stringRedisTemplate);
}
}
@@ -0,0 +1,60 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.redis.debounce;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.data.redis.core.StringRedisTemplate;
/**
* 防抖自动化配置
*
* @author BladeX
*/
@AutoConfiguration
@ConditionalOnBean(StringRedisTemplate.class)
@EnableConfigurationProperties(RedisDebounceProperties.class)
@ConditionalOnProperty(value = RedisDebounceProperties.PREFIX + ".enabled", havingValue = "true", matchIfMissing = true)
public class BladeDebounceAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public DebounceClient redisDebounceClient(StringRedisTemplate stringRedisTemplate, RedisDebounceProperties properties) {
return new RedisDebounceClient(stringRedisTemplate, properties);
}
@Bean
@ConditionalOnMissingBean
public RedisDebounceAspect redisDebounceAspect(DebounceClient debounceClient, RedisDebounceProperties properties) {
return new RedisDebounceAspect(debounceClient, properties);
}
}
@@ -0,0 +1,82 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.redis.debounce;
import java.util.concurrent.TimeUnit;
/**
* 防抖客户端
*
* @author BladeX
*/
public interface DebounceClient {
/**
* 尝试防抖,如果在防抖时间内则返回false
*
* @param key 防抖键
* @param interval 防抖间隔
* @param timeUnit 时间单位
* @return true-可以执行,false-在防抖期内
*/
boolean tryDebounce(String key, long interval, TimeUnit timeUnit);
/**
* 获取防抖剩余时间
*
* @param key 防抖键
* @param timeUnit 时间单位
* @return 剩余时间,-1表示不在防抖期内
*/
long getRemainingTime(String key, TimeUnit timeUnit);
/**
* 清除防抖记录
*
* @param key 防抖键
* @return 是否成功清除
*/
boolean clearDebounce(String key);
/**
* 检查是否在防抖期内
*
* @param key 防抖键
* @return true-在防抖期内,false-不在防抖期内
*/
boolean isInDebounce(String key);
/**
* 设置防抖记录(不管是否已存在)
*
* @param key 防抖键
* @param interval 防抖间隔
* @param timeUnit 时间单位
*/
void setDebounce(String key, long interval, TimeUnit timeUnit);
}
@@ -0,0 +1,85 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.redis.debounce;
import lombok.Getter;
import java.util.concurrent.TimeUnit;
/**
* 防抖异常
*
* @author BladeX
*/
@Getter
public class DebounceException extends RuntimeException {
/**
* 防抖 key
*/
private final String debounceKey;
/**
* 剩余时间(秒)
*/
private final long remainingTime;
/**
* 时间单位
*/
private final TimeUnit timeUnit;
public DebounceException(String message, String debounceKey, long remainingTime, TimeUnit timeUnit) {
super(message);
this.debounceKey = debounceKey;
this.remainingTime = remainingTime;
this.timeUnit = timeUnit;
}
public DebounceException(String message, String debounceKey) {
this(message, debounceKey, 0, TimeUnit.SECONDS);
}
/**
* 获取剩余时间(秒)
*
* @return 剩余时间
*/
public long getRemainingSeconds() {
return timeUnit.toSeconds(remainingTime);
}
/**
* 获取剩余时间(毫秒)
*
* @return 剩余时间
*/
public long getRemainingMillis() {
return timeUnit.toMillis(remainingTime);
}
}
@@ -0,0 +1,197 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.redis.debounce;
import jakarta.servlet.Servlet;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.springblade.core.secure.utils.AuthUtil;
import org.springblade.core.tool.api.R;
import org.springblade.core.tool.utils.WebUtil;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.DispatcherServlet;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* 防抖异常处理器
*
* @author BladeX
*/
@Slf4j
@Order(Ordered.HIGHEST_PRECEDENCE + 1)
@AutoConfiguration
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
@ConditionalOnClass({Servlet.class, DispatcherServlet.class})
@RestControllerAdvice
public class DebounceExceptionTranslator {
/**
* 处理防抖异常
*/
@ExceptionHandler(DebounceException.class)
@ResponseStatus(HttpStatus.TOO_MANY_REQUESTS)
public R<String> handleError(DebounceException e) {
DebounceLogBuilder logBuilder = DebounceLogBuilder.create()
.debounceKey(e.getDebounceKey())
.message(e.getMessage());
if (hasRemainingTimeInfo(e)) {
logBuilder.withRemainingTime(e.getRemainingTime(), e.getTimeUnit())
.remainingSeconds(e.getRemainingSeconds())
.remainingMillis(e.getRemainingMillis());
}
logBuilder.withRequestInfo().build().log();
return R.fail(HttpStatus.TOO_MANY_REQUESTS.value(), e.getMessage());
}
/**
* 判断异常是否包含剩余时间信息
*/
private boolean hasRemainingTimeInfo(DebounceException e) {
return e.getRemainingTime() > 0 && e.getTimeUnit() != null;
}
/**
* 防抖日志构建器
*/
private static class DebounceLogBuilder {
private final StringBuilder template = new StringBuilder("防抖异常 - 操作过于频繁:");
private final List<Object> parameters = new ArrayList<>();
private DebounceLogBuilder() {
}
public static DebounceLogBuilder create() {
return new DebounceLogBuilder();
}
public DebounceLogBuilder debounceKey(String key) {
template.append("\n 防抖键值: {}");
parameters.add(key);
return this;
}
public DebounceLogBuilder withRemainingTime(long remainingTime, TimeUnit timeUnit) {
template.append("\n 剩余时间: {} {}");
parameters.add(remainingTime);
parameters.add(timeUnit.name().toLowerCase());
return this;
}
public DebounceLogBuilder remainingSeconds(long seconds) {
template.append("\n 剩余秒数: {} 秒");
parameters.add(seconds);
return this;
}
public void remainingMillis(long millis) {
template.append("\n 剩余毫秒: {} 毫秒");
parameters.add(millis);
}
public DebounceLogBuilder message(String message) {
template.append("\n 异常消息: {}");
parameters.add(message);
return this;
}
public DebounceLogBuilder withRequestInfo() {
HttpServletRequest request = WebUtil.getRequest();
if (request != null) {
appendRequestUrl(request);
appendRequestMethod(request);
appendRequestParameter(request);
appendClientIp(request);
appendRequestUser(request);
appendRequestUserId(request);
}
return this;
}
private void appendRequestUrl(HttpServletRequest request) {
template.append("\n 请求URL: {}");
String requestUrl = request.getRequestURL().toString();
String queryString = request.getQueryString();
String fullUrl = queryString != null && !queryString.isEmpty() ?
requestUrl + "?" + queryString : requestUrl;
parameters.add(fullUrl);
}
private void appendRequestMethod(HttpServletRequest request) {
template.append("\n 请求方法: {}");
parameters.add(request.getMethod());
}
private void appendRequestParameter(HttpServletRequest request) {
template.append("\n 请求参数: {}");
parameters.add(WebUtil.getRequestContent(request));
}
private void appendClientIp(HttpServletRequest request) {
template.append("\n 请求地址: {}");
parameters.add(WebUtil.getIP(request));
}
private void appendRequestUser(HttpServletRequest request) {
template.append("\n 请求用户: {}");
parameters.add(AuthUtil.getUserAccount(request));
}
private void appendRequestUserId(HttpServletRequest request) {
template.append("\n 用户主键: {}");
parameters.add(AuthUtil.getUserId(request));
}
public LogEntry build() {
return new LogEntry(template.toString(), parameters.toArray());
}
}
/**
* 日志条目
*/
private record LogEntry(String template, Object[] parameters) {
public void log() {
log.error(template, parameters);
}
}
}
@@ -0,0 +1,100 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.redis.debounce;
import java.lang.annotation.*;
import java.util.concurrent.TimeUnit;
/**
* Redis 防抖注解
*
* <p>
* 在指定时间窗口内,相同 key 的请求只会执行一次,后续请求将被忽略。
* 适用于防止重复提交、频繁调用等场景。
* </p>
*
* <p>使用示例:</p>
* <pre>
* {@code
* @RedisDebounce(key = "sms:send", param = "#phone", interval = 60)
* public void sendSms(String phone) {
* // 60秒内同一手机号只能发送一次短信
* }
* }
* </pre>
*
* @author BladeX
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface RedisDebounce {
/**
* 防抖的 key,必须:请保持唯一性
*
* @return key
*/
String key();
/**
* 防抖参数,可选,支持 spring el # 读取方法参数和 @ 读取 spring bean
*
* @return param
*/
String param() default "";
/**
* 防抖时间间隔,默认60秒
*
* @return 时间间隔
*/
long interval() default 60;
/**
* 时间单位,默认为秒
*
* @return 时间单位
*/
TimeUnit timeUnit() default TimeUnit.SECONDS;
/**
* 是否返回剩余时间信息,默认false
* 如果为true,当防抖生效时会抛出包含剩余时间信息的异常
*
* @return 是否返回剩余时间
*/
boolean includeRemainingTime() default false;
/**
* 自定义防抖生效时的提示信息
*
* @return 提示信息
*/
String message() default "操作过于频繁,请稍后再试";
}
@@ -0,0 +1,160 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.redis.debounce;
import lombok.RequiredArgsConstructor;
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.springblade.core.tool.spel.BladeExpressionEvaluator;
import org.springblade.core.tool.utils.CharPool;
import org.springblade.core.tool.utils.StringUtil;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.expression.AnnotatedElementKey;
import org.springframework.expression.EvaluationContext;
import org.springframework.util.Assert;
import java.lang.reflect.Method;
import java.util.concurrent.TimeUnit;
/**
* Redis 防抖切面
*
* @author BladeX
*/
@Slf4j
@Aspect
@RequiredArgsConstructor
public class RedisDebounceAspect implements ApplicationContextAware {
/**
* 表达式处理
*/
private static final BladeExpressionEvaluator EVALUATOR = new BladeExpressionEvaluator();
/**
* Redis 防抖服务
*/
private final DebounceClient debounceClient;
/**
* Redis 防抖配置
*/
private final RedisDebounceProperties properties;
private ApplicationContext applicationContext;
/**
* AOP 环切 注解 @RedisDebounce
*/
@Around("@annotation(redisDebounce)")
public Object aroundRedisDebounce(ProceedingJoinPoint point, RedisDebounce redisDebounce) throws Throwable {
String debounceKey = redisDebounce.key();
Assert.hasText(debounceKey, "@RedisDebounce key must have length; it must not be null or empty");
// 构建完整的防抖键
String fullDebounceKey = buildDebounceKey(point, redisDebounce);
// 防抖参数
long interval = redisDebounce.interval();
TimeUnit timeUnit = redisDebounce.timeUnit();
String message = redisDebounce.message();
boolean includeRemainingTime = redisDebounce.includeRemainingTime();
// 尝试防抖
boolean canExecute = debounceClient.tryDebounce(fullDebounceKey, interval, timeUnit);
if (!canExecute) {
// 在防抖期内,不执行方法
if (properties.getEnableDebugLog()) {
log.debug("Method execution skipped due to debounce: {}", fullDebounceKey);
}
if (includeRemainingTime) {
long remainingTime = debounceClient.getRemainingTime(fullDebounceKey, timeUnit);
throw new DebounceException(message, fullDebounceKey, remainingTime, timeUnit);
} else {
throw new DebounceException(message, fullDebounceKey);
}
}
// 不在防抖期内,执行方法
if (properties.getEnableDebugLog()) {
log.debug("Method execution allowed: {}", fullDebounceKey);
}
return point.proceed();
}
/**
* 构建防抖键
*
* @param point 切点
* @param redisDebounce 防抖注解
* @return 防抖键
*/
private String buildDebounceKey(ProceedingJoinPoint point, RedisDebounce redisDebounce) {
String baseKey = redisDebounce.key();
String param = redisDebounce.param();
// 如果没有参数,直接返回基础键
if (StringUtil.isBlank(param)) {
return baseKey;
}
// 解析参数表达式
String paramValue = evalDebounceParam(point, param);
return baseKey + CharPool.COLON + paramValue;
}
/**
* 计算参数表达式
*
* @param point ProceedingJoinPoint
* @param debounceParam 防抖参数
* @return 结果
*/
private String evalDebounceParam(ProceedingJoinPoint point, String debounceParam) {
MethodSignature ms = (MethodSignature) point.getSignature();
Method method = ms.getMethod();
Object[] args = point.getArgs();
Object target = point.getTarget();
Class<?> targetClass = target.getClass();
EvaluationContext context = EVALUATOR.createContext(method, args, target, targetClass, applicationContext);
AnnotatedElementKey elementKey = new AnnotatedElementKey(method, targetClass);
return EVALUATOR.evalAsText(debounceParam, elementKey, context);
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
}
@@ -0,0 +1,102 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.redis.debounce;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import java.time.Duration;
import java.util.concurrent.TimeUnit;
/**
* Redis 防抖客户端实现
*
* @author BladeX
*/
@Slf4j
@RequiredArgsConstructor
public class RedisDebounceClient implements DebounceClient {
private static final String DEBOUNCE_VALUE = "1";
private final StringRedisTemplate redisTemplate;
private final RedisDebounceProperties properties;
@Override
public boolean tryDebounce(String key, long interval, TimeUnit timeUnit) {
String fullKey = buildKey(key);
Duration duration = Duration.of(interval, timeUnit.toChronoUnit());
// 使用 SET NX EX 命令,如果键不存在则设置并返回true,存在则返回false
Boolean result = redisTemplate.opsForValue().setIfAbsent(fullKey, DEBOUNCE_VALUE, duration);
return Boolean.TRUE.equals(result);
}
@Override
public long getRemainingTime(String key, TimeUnit timeUnit) {
String fullKey = buildKey(key);
long ttl = redisTemplate.getExpire(fullKey, TimeUnit.SECONDS);
if (ttl <= 0) {
return -1;
}
return timeUnit.convert(ttl, TimeUnit.SECONDS);
}
@Override
public boolean clearDebounce(String key) {
String fullKey = buildKey(key);
return redisTemplate.delete(fullKey);
}
@Override
public boolean isInDebounce(String key) {
String fullKey = buildKey(key);
return redisTemplate.hasKey(fullKey);
}
@Override
public void setDebounce(String key, long interval, TimeUnit timeUnit) {
String fullKey = buildKey(key);
Duration duration = Duration.of(interval, timeUnit.toChronoUnit());
redisTemplate.opsForValue().set(fullKey, DEBOUNCE_VALUE, duration);
}
/**
* 构建完整的 Redis 键
*
* @param key 原始键
* @return 完整的 Redis 键
*/
private String buildKey(String key) {
return properties.getKeyPrefix() + key;
}
}
@@ -0,0 +1,64 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.redis.debounce;
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* Redis 防抖配置属性
*
* @author BladeX
*/
@Getter
@Setter
@ConfigurationProperties(RedisDebounceProperties.PREFIX)
public class RedisDebounceProperties {
public static final String PREFIX = "blade.redis.debounce";
/**
* 是否开启防抖功能,默认为:true
*/
private Boolean enabled = Boolean.TRUE;
/**
* 防抖键前缀,默认为:blade:debounce:
*/
private String keyPrefix = "blade:debounce:";
/**
* 默认防抖间隔时间(秒),默认为:60秒
*/
private Long defaultInterval = 60L;
/**
* 是否启用调试日志,默认为:false
*/
private Boolean enableDebugLog = Boolean.FALSE;
}
@@ -0,0 +1,161 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: DreamLu (596392912@qq.com)
*/
package org.springblade.core.redis.lock;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.*;
import org.springblade.core.tool.utils.StringUtil;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
/**
* 分布式锁自动化配置
*
* @author L.cm
*/
@AutoConfiguration
@ConditionalOnClass(RedissonClient.class)
@EnableConfigurationProperties(BladeLockProperties.class)
@ConditionalOnProperty(value = "blade.lock.enabled", havingValue = "true")
public class BladeLockAutoConfiguration {
private static Config singleConfig(BladeLockProperties properties) {
Config config = new Config();
SingleServerConfig serversConfig = config.useSingleServer();
serversConfig.setAddress(properties.getAddress());
String password = properties.getPassword();
if (StringUtil.isNotBlank(password)) {
serversConfig.setPassword(password);
}
serversConfig.setDatabase(properties.getDatabase());
serversConfig.setConnectionPoolSize(properties.getPoolSize());
serversConfig.setConnectionMinimumIdleSize(properties.getIdleSize());
serversConfig.setIdleConnectionTimeout(properties.getConnectionTimeout());
serversConfig.setConnectTimeout(properties.getConnectionTimeout());
serversConfig.setTimeout(properties.getTimeout());
return config;
}
private static Config masterSlaveConfig(BladeLockProperties properties) {
Config config = new Config();
MasterSlaveServersConfig serversConfig = config.useMasterSlaveServers();
serversConfig.setMasterAddress(properties.getMasterAddress());
serversConfig.addSlaveAddress(properties.getSlaveAddress());
String password = properties.getPassword();
if (StringUtil.isNotBlank(password)) {
serversConfig.setPassword(password);
}
serversConfig.setDatabase(properties.getDatabase());
serversConfig.setMasterConnectionPoolSize(properties.getPoolSize());
serversConfig.setMasterConnectionMinimumIdleSize(properties.getIdleSize());
serversConfig.setSlaveConnectionPoolSize(properties.getPoolSize());
serversConfig.setSlaveConnectionMinimumIdleSize(properties.getIdleSize());
serversConfig.setIdleConnectionTimeout(properties.getConnectionTimeout());
serversConfig.setConnectTimeout(properties.getConnectionTimeout());
serversConfig.setTimeout(properties.getTimeout());
return config;
}
private static Config sentinelConfig(BladeLockProperties properties) {
Config config = new Config();
SentinelServersConfig serversConfig = config.useSentinelServers();
serversConfig.setMasterName(properties.getMasterName());
serversConfig.addSentinelAddress(properties.getSentinelAddress());
String password = properties.getPassword();
if (StringUtil.isNotBlank(password)) {
serversConfig.setPassword(password);
}
serversConfig.setDatabase(properties.getDatabase());
serversConfig.setMasterConnectionPoolSize(properties.getPoolSize());
serversConfig.setMasterConnectionMinimumIdleSize(properties.getIdleSize());
serversConfig.setSlaveConnectionPoolSize(properties.getPoolSize());
serversConfig.setSlaveConnectionMinimumIdleSize(properties.getIdleSize());
serversConfig.setIdleConnectionTimeout(properties.getConnectionTimeout());
serversConfig.setConnectTimeout(properties.getConnectionTimeout());
serversConfig.setTimeout(properties.getTimeout());
return config;
}
private static Config clusterConfig(BladeLockProperties properties) {
Config config = new Config();
ClusterServersConfig serversConfig = config.useClusterServers();
serversConfig.addNodeAddress(properties.getNodeAddress());
String password = properties.getPassword();
if (StringUtil.isNotBlank(password)) {
serversConfig.setPassword(password);
}
serversConfig.setMasterConnectionPoolSize(properties.getPoolSize());
serversConfig.setMasterConnectionMinimumIdleSize(properties.getIdleSize());
serversConfig.setSlaveConnectionPoolSize(properties.getPoolSize());
serversConfig.setSlaveConnectionMinimumIdleSize(properties.getIdleSize());
serversConfig.setIdleConnectionTimeout(properties.getConnectionTimeout());
serversConfig.setConnectTimeout(properties.getConnectionTimeout());
serversConfig.setTimeout(properties.getTimeout());
return config;
}
@Bean
@ConditionalOnMissingBean
public RedisLockClient redisLockClient(BladeLockProperties properties) {
return new RedisLockClientImpl(redissonClient(properties));
}
@Bean
@ConditionalOnMissingBean
public RedisLockAspect redisLockAspect(RedisLockClient redisLockClient) {
return new RedisLockAspect(redisLockClient);
}
private static RedissonClient redissonClient(BladeLockProperties properties) {
BladeLockProperties.Mode mode = properties.getMode();
Config config;
switch (mode) {
case sentinel:
config = sentinelConfig(properties);
break;
case cluster:
config = clusterConfig(properties);
break;
case master:
config = masterSlaveConfig(properties);
break;
case single:
config = singleConfig(properties);
break;
default:
config = new Config();
break;
}
return Redisson.create(config);
}
}
@@ -0,0 +1,114 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: DreamLu (596392912@qq.com)
*/
package org.springblade.core.redis.lock;
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* 分布式锁配置
*
* @author L.cm
*/
@Getter
@Setter
@ConfigurationProperties(BladeLockProperties.PREFIX)
public class BladeLockProperties {
public static final String PREFIX = "blade.lock";
/**
* 是否开启:默认为:false,便于生成配置提示。
*/
private Boolean enabled = Boolean.FALSE;
/**
* 单机配置:redis 服务地址
*/
private String address = "redis://127.0.0.1:6379";
/**
* 密码配置
*/
private String password;
/**
* db
*/
private Integer database = 0;
/**
* 连接池大小
*/
private Integer poolSize = 20;
/**
* 最小空闲连接数
*/
private Integer idleSize = 5;
/**
* 连接空闲超时,单位:毫秒
*/
private Integer idleTimeout = 60000;
/**
* 连接超时,单位:毫秒
*/
private Integer connectionTimeout = 3000;
/**
* 命令等待超时,单位:毫秒
*/
private Integer timeout = 10000;
/**
* 集群模式,单机:single,主从:master,哨兵模式:sentinel,集群模式:cluster
*/
private Mode mode = Mode.single;
/**
* 主从模式,主地址
*/
private String masterAddress;
/**
* 主从模式,从地址
*/
private String[] slaveAddress;
/**
* 哨兵模式:主名称
*/
private String masterName;
/**
* 哨兵模式地址
*/
private String[] sentinelAddress;
/**
* 集群模式节点地址
*/
private String[] nodeAddress;
public enum Mode {
/**
* 集群模式,单机:single,主从:master,哨兵模式:sentinel,集群模式:cluster
*/
single,
master,
sentinel,
cluster
}
}
@@ -0,0 +1,43 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: DreamLu (596392912@qq.com)
*/
package org.springblade.core.redis.lock;
/**
* 锁类型
*
* @author lcm
*/
public enum LockType {
/**
* 重入锁
*/
REENTRANT,
/**
* 公平锁
*/
FAIR
}
@@ -0,0 +1,91 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: DreamLu (596392912@qq.com)
*/
package org.springblade.core.redis.lock;
import java.lang.annotation.*;
import java.util.concurrent.TimeUnit;
/**
* 分布式锁注解,redisson,支持的锁的种类有很多,适合注解形式的只有重入锁、公平锁
*
* <p>
* 1. 可重入锁(Reentrant Lock
* 2. 公平锁(Fair Lock
* 3. 联锁(MultiLock
* 4. 红锁(RedLock
* 5. 读写锁(ReadWriteLock
* </p>
*
* @author L.cm
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface RedisLock {
/**
* 分布式锁的 key,必须:请保持唯一性
*
* @return key
*/
String value();
/**
* 分布式锁参数,可选,支持 spring el # 读取方法参数和 @ 读取 spring bean
*
* @return param
*/
String param() default "";
/**
* 等待锁超时时间,默认30
*
* @return int
*/
long waitTime() default 30;
/**
* 自动解锁时间,自动解锁时间一定得大于方法执行时间,否则会导致锁提前释放,默认100
*
* @return int
*/
long leaseTime() default 100;
/**
* 时间单位,默认为秒
*
* @return 时间单位
*/
TimeUnit timeUnit() default TimeUnit.SECONDS;
/**
* 默认公平锁
*
* @return LockType
*/
LockType type() default LockType.FAIR;
}
@@ -0,0 +1,112 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: DreamLu (596392912@qq.com)
*/
package org.springblade.core.redis.lock;
import lombok.RequiredArgsConstructor;
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.springblade.core.tool.spel.BladeExpressionEvaluator;
import org.springblade.core.tool.utils.CharPool;
import org.springblade.core.tool.utils.StringUtil;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.expression.AnnotatedElementKey;
import org.springframework.expression.EvaluationContext;
import org.springframework.util.Assert;
import java.lang.reflect.Method;
import java.util.concurrent.TimeUnit;
/**
* redis 分布式锁
*
* @author L.cm
*/
@Aspect
@RequiredArgsConstructor
public class RedisLockAspect implements ApplicationContextAware {
/**
* 表达式处理
*/
private static final BladeExpressionEvaluator EVALUATOR = new BladeExpressionEvaluator();
/**
* redis 限流服务
*/
private final RedisLockClient redisLockClient;
private ApplicationContext applicationContext;
/**
* AOP 环切 注解 @RedisLock
*/
@Around("@annotation(redisLock)")
public Object aroundRedisLock(ProceedingJoinPoint point, RedisLock redisLock) {
String lockName = redisLock.value();
Assert.hasText(lockName, "@RedisLock value must have length; it must not be null or empty");
// el 表达式
String lockParam = redisLock.param();
// 表达式不为空
String lockKey;
if (StringUtil.isNotBlank(lockParam)) {
String evalAsText = evalLockParam(point, lockParam);
lockKey = lockName + CharPool.COLON + evalAsText;
} else {
lockKey = lockName;
}
LockType lockType = redisLock.type();
long waitTime = redisLock.waitTime();
long leaseTime = redisLock.leaseTime();
TimeUnit timeUnit = redisLock.timeUnit();
return redisLockClient.lock(lockKey, lockType, waitTime, leaseTime, timeUnit, point::proceed);
}
/**
* 计算参数表达式
*
* @param point ProceedingJoinPoint
* @param lockParam lockParam
* @return 结果
*/
private String evalLockParam(ProceedingJoinPoint point, String lockParam) {
MethodSignature ms = (MethodSignature) point.getSignature();
Method method = ms.getMethod();
Object[] args = point.getArgs();
Object target = point.getTarget();
Class<?> targetClass = target.getClass();
EvaluationContext context = EVALUATOR.createContext(method, args, target, targetClass, applicationContext);
AnnotatedElementKey elementKey = new AnnotatedElementKey(method, targetClass);
return EVALUATOR.evalAsText(lockParam, elementKey, context);
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
}
@@ -0,0 +1,100 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: DreamLu (596392912@qq.com)
*/
package org.springblade.core.redis.lock;
import org.springblade.core.tool.function.CheckedSupplier;
import java.util.concurrent.TimeUnit;
/**
* 锁客户端
*
* @author L.cm
*/
public interface RedisLockClient {
/**
* 尝试获取锁
*
* @param lockName 锁名
* @param lockType 锁类型
* @param waitTime 等待时间
* @param leaseTime 自动解锁时间,自动解锁时间一定得大于方法执行时间
* @param timeUnit 时间参数
* @return 是否成功
* @throws InterruptedException InterruptedException
*/
boolean tryLock(String lockName, LockType lockType, long waitTime, long leaseTime, TimeUnit timeUnit) throws InterruptedException;
/**
* 解锁
*
* @param lockName 锁名
* @param lockType 锁类型
*/
void unLock(String lockName, LockType lockType);
/**
* 自定获取锁后执行方法
*
* @param lockName 锁名
* @param lockType 锁类型
* @param waitTime 等待锁超时时间
* @param leaseTime 自动解锁时间,自动解锁时间一定得大于方法执行时间,否则会导致锁提前释放,默认100
* @param timeUnit 时间单位
* @param supplier 获取锁后的回调
* @return 返回的数据
*/
<T> T lock(String lockName, LockType lockType, long waitTime, long leaseTime, TimeUnit timeUnit, CheckedSupplier<T> supplier);
/**
* 公平锁
*
* @param lockName 锁名
* @param waitTime 等待锁超时时间
* @param leaseTime 自动解锁时间,自动解锁时间一定得大于方法执行时间,否则会导致锁提前释放,默认100
* @param supplier 获取锁后的回调
* @return 返回的数据
*/
default <T> T lockFair(String lockName, long waitTime, long leaseTime, CheckedSupplier<T> supplier) {
return lock(lockName, LockType.FAIR, waitTime, leaseTime, TimeUnit.SECONDS, supplier);
}
/**
* 可重入锁
*
* @param lockName 锁名
* @param waitTime 等待锁超时时间
* @param leaseTime 自动解锁时间,自动解锁时间一定得大于方法执行时间,否则会导致锁提前释放,默认100
* @param supplier 获取锁后的回调
* @return 返回的数据
*/
default <T> T lockReentrant(String lockName, long waitTime, long leaseTime, CheckedSupplier<T> supplier) {
return lock(lockName, LockType.REENTRANT, waitTime, leaseTime, TimeUnit.SECONDS, supplier);
}
}
@@ -0,0 +1,88 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: DreamLu (596392912@qq.com)
*/
package org.springblade.core.redis.lock;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springblade.core.tool.function.CheckedSupplier;
import org.springblade.core.tool.utils.Exceptions;
import java.util.concurrent.TimeUnit;
/**
* 锁客户端
*
* @author L.cm
*/
@Slf4j
@RequiredArgsConstructor
public class RedisLockClientImpl implements RedisLockClient {
private final RedissonClient redissonClient;
@Override
public boolean tryLock(String lockName, LockType lockType, long waitTime, long leaseTime, TimeUnit timeUnit) throws InterruptedException {
RLock lock = getLock(lockName, lockType);
return lock.tryLock(waitTime, leaseTime, timeUnit);
}
@Override
public void unLock(String lockName, LockType lockType) {
RLock lock = getLock(lockName, lockType);
// 仅仅在已经锁定和当前线程持有锁时解锁
if (lock.isLocked() && lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
private RLock getLock(String lockName, LockType lockType) {
RLock lock;
if (LockType.REENTRANT == lockType) {
lock = redissonClient.getLock(lockName);
} else {
lock = redissonClient.getFairLock(lockName);
}
return lock;
}
@Override
public <T> T lock(String lockName, LockType lockType, long waitTime, long leaseTime, TimeUnit timeUnit, CheckedSupplier<T> supplier) {
try {
boolean result = this.tryLock(lockName, lockType, waitTime, leaseTime, timeUnit);
if (result) {
return supplier.get();
}
} catch (Throwable e) {
throw Exceptions.unchecked(e);
} finally {
this.unLock(lockName, lockType);
}
return null;
}
}
@@ -0,0 +1,109 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: DreamLu (596392912@qq.com)
*/
package org.springblade.core.redis.pubsub;
import lombok.experimental.UtilityClass;
import org.springblade.core.tool.utils.CharPool;
import org.springframework.data.redis.listener.ChannelTopic;
import org.springframework.data.redis.listener.PatternTopic;
import org.springframework.data.redis.listener.Topic;
/**
* channel 工具类
*
* @author L.cm
*/
@UtilityClass
class ChannelUtil {
/**
* 获取 pub sub topic
*
* @param channel channel
* @return Topic
*/
public static Topic getTopic(String channel) {
return isPattern(channel) ? new PatternTopic(channel) : new ChannelTopic(channel);
}
/**
* 判断是否为模糊话题,*、? 和 [...]
*
* @param channel 话题名
* @return 是否模糊话题
*/
public static boolean isPattern(String channel) {
int length = channel.length();
boolean isRightSqBracket = false;
// 倒序,因为表达式一般在尾部
for (int i = length - 1; i > 0; i--) {
char charAt = channel.charAt(i);
switch (charAt) {
case CharPool.ASTERISK:
case CharPool.QUESTION_MARK:
if (isEscapeChars(channel, i)) {
break;
}
return true;
case CharPool.RIGHT_SQ_BRACKET:
if (isEscapeChars(channel, i)) {
break;
}
isRightSqBracket = true;
break;
case CharPool.LEFT_SQ_BRACKET:
if (isEscapeChars(channel, i)) {
break;
}
if (isRightSqBracket) {
return true;
}
break;
default:
break;
}
}
return false;
}
/**
* 判断是否为转义字符
*
* @param name 话题名
* @param index 索引
* @return 是否为转义字符
*/
private static boolean isEscapeChars(String name, int index) {
if (index < 1) {
return false;
}
// 预读一位,判断是否为转义符 “/”
char charAt = name.charAt(index - 1);
return CharPool.BACK_SLASH == charAt;
}
}
@@ -0,0 +1,55 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: DreamLu (596392912@qq.com)
*/
package org.springblade.core.redis.pubsub;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.ToString;
/**
* 基于 redis pub sub 事件对象
*
* @author L.cm
*/
@Getter
@ToString
@RequiredArgsConstructor
public class RPubSubEvent<M> {
/**
* 匹配模式时的正则
*/
private final CharSequence pattern;
/**
* channel
*/
private final CharSequence channel;
/**
* pub 的消息对象
*/
private final M msg;
}
@@ -0,0 +1,49 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: DreamLu (596392912@qq.com)
*/
package org.springblade.core.redis.pubsub;
import java.lang.annotation.*;
/**
* 基于 Redisson 的消息监听器
*
* @author L.cm
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface RPubSubListener {
/**
* topic name,支持通配符, 如 *、? 和 [...]
*
* @return String
*/
String value();
}
@@ -0,0 +1,91 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: DreamLu (596392912@qq.com)
*/
package org.springblade.core.redis.pubsub;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springblade.core.tool.utils.ReflectUtil;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.listener.Topic;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ReflectionUtils;
import java.lang.reflect.Method;
/**
* Redisson 监听器
*
* @author L.cm
*/
@Slf4j
@RequiredArgsConstructor
public class RPubSubListenerDetector implements BeanPostProcessor {
private final RedisMessageListenerContainer redisMessageListenerContainer;
private final RedisSerializer<Object> redisSerializer;
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
Class<?> userClass = ClassUtils.getUserClass(bean);
ReflectionUtils.doWithMethods(userClass, method -> {
RPubSubListener listener = AnnotationUtils.findAnnotation(method, RPubSubListener.class);
if (listener != null) {
String channel = listener.value();
Assert.hasText(channel, "@RPubSubListener value channel must not be empty.");
log.info("Found @RPubSubListener on bean:{} method:{}", beanName, method);
// 校验 methodmethod 入参数大于等于1
int paramCount = method.getParameterCount();
if (paramCount > 1) {
throw new IllegalArgumentException("@RPubSubListener on method " + method + " parameter count must less or equal to 1.");
}
// 精准模式和模糊匹配模式
Topic topic = ChannelUtil.getTopic(channel);
redisMessageListenerContainer.addMessageListener((message, pattern) -> {
String messageChannel = new String(message.getChannel());
Object body = redisSerializer.deserialize(message.getBody());
invokeMethod(bean, method, paramCount, new RPubSubEvent<>(channel, messageChannel, body));
}, topic);
}
}, ReflectionUtils.USER_DECLARED_METHODS);
return bean;
}
private static void invokeMethod(Object bean, Method method, int paramCount, RPubSubEvent<Object> topicEvent) {
// 支持没有参数的方法
if (paramCount == 0) {
ReflectUtil.invokeMethod(method, bean);
} else {
ReflectUtil.invokeMethod(method, bean, topicEvent);
}
}
}
@@ -0,0 +1,63 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: DreamLu (596392912@qq.com)
*/
package org.springblade.core.redis.pubsub;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.boot.LazyInitializationExcludeFilter;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.util.ReflectionUtils;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
/**
* mqtt 客户端订阅延迟加载排除
*
* @author L.cm
*/
public class RPubSubListenerLazyFilter implements LazyInitializationExcludeFilter {
@Override
public boolean isExcluded(String beanName, BeanDefinition beanDefinition, Class<?> beanType) {
// 类上有注解的情况
RPubSubListener subscribe = AnnotationUtils.findAnnotation(beanType, RPubSubListener.class);
if (subscribe != null) {
return true;
}
// 方法上的注解
List<Method> methodList = new ArrayList<>();
ReflectionUtils.doWithMethods(beanType, method -> {
RPubSubListener clientSubscribe = AnnotationUtils.findAnnotation(method, RPubSubListener.class);
if (clientSubscribe != null) {
methodList.add(method);
}
}, ReflectionUtils.USER_DECLARED_METHODS);
return !methodList.isEmpty();
}
}
@@ -0,0 +1,45 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: DreamLu (596392912@qq.com)
*/
package org.springblade.core.redis.pubsub;
/**
* 基于 Redisson 的消息发布器
*
* @author L.cm
*/
public interface RPubSubPublisher {
/**
* 发布消息
*
* @param channel 队列名
* @param message 消息
* @return 收到消息的客户数量
*/
<T> Long publish(String channel, T message);
}
@@ -0,0 +1,55 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: DreamLu (596392912@qq.com)
*/
package org.springblade.core.redis.pubsub;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springblade.core.redis.cache.BladeRedis;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.data.redis.serializer.RedisSerializer;
/**
* Redisson pub/sub 发布器
*
* @author L.cm
*/
@Slf4j
@RequiredArgsConstructor
public class RedisPubSubPublisher implements InitializingBean, RPubSubPublisher {
private final BladeRedis bladeRedis;
private final RedisSerializer<Object> redisSerializer;
@Override
public <T> Long publish(String channel, T message) {
return bladeRedis.publish(channel, message, redisSerializer::serialize);
}
@Override
public void afterPropertiesSet() throws Exception {
log.info("RPubSubPublisher init success.");
}
}
@@ -0,0 +1,76 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: DreamLu (596392912@qq.com)
*/
package org.springblade.core.redis.ratelimiter;
import java.lang.annotation.*;
import java.util.concurrent.TimeUnit;
/**
* 分布式 限流注解,默认速率为 600/ms
*
* @author L.cm
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface RateLimiter {
/**
* 限流的 key 支持,必须:请保持唯一性
*
* @return key
*/
String value();
/**
* 限流的参数,可选,支持 spring el # 读取方法参数和 @ 读取 spring bean
*
* @return param
*/
String param() default "";
/**
* 支持的最大请求,默认: 100
*
* @return 请求数
*/
long max() default 100L;
/**
* 持续时间,默认: 3600
*
* @return 持续时间
*/
long ttl() default 1L;
/**
* 时间单位,默认为分
*
* @return TimeUnit
*/
TimeUnit timeUnit() default TimeUnit.MINUTES;
}
@@ -0,0 +1,100 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: DreamLu (596392912@qq.com)
*/
package org.springblade.core.redis.ratelimiter;
import org.springblade.core.tool.function.CheckedSupplier;
import org.springblade.core.tool.utils.Exceptions;
import java.util.concurrent.TimeUnit;
/**
* RateLimiter 限流 Client
*
* @author L.cm
*/
public interface RateLimiterClient {
/**
* 服务是否被限流
*
* @param key 自定义的key,请保证唯一
* @param max 支持的最大请求
* @param ttl 时间,单位默认为秒(seconds
* @return 是否允许
*/
default boolean isAllowed(String key, long max, long ttl) {
return this.isAllowed(key, max, ttl, TimeUnit.SECONDS);
}
/**
* 服务是否被限流
*
* @param key 自定义的key,请保证唯一
* @param max 支持的最大请求
* @param ttl 时间
* @param timeUnit 时间单位
* @return 是否允许
*/
boolean isAllowed(String key, long max, long ttl, TimeUnit timeUnit);
/**
* 服务限流,被限制时抛出 RateLimiterException 异常,需要自行处理异常
*
* @param key 自定义的key,请保证唯一
* @param max 支持的最大请求
* @param ttl 时间
* @param supplier Supplier 函数式
* @return 函数执行结果
*/
default <T> T allow(String key, long max, long ttl, CheckedSupplier<T> supplier) {
return allow(key, max, ttl, TimeUnit.SECONDS, supplier);
}
/**
* 服务限流,被限制时抛出 RateLimiterException 异常,需要自行处理异常
*
* @param key 自定义的key,请保证唯一
* @param max 支持的最大请求
* @param ttl 时间
* @param timeUnit 时间单位
* @param supplier Supplier 函数式
* @param <T>
* @return 函数执行结果
*/
default <T> T allow(String key, long max, long ttl, TimeUnit timeUnit, CheckedSupplier<T> supplier) {
boolean isAllowed = this.isAllowed(key, max, ttl, timeUnit);
if (isAllowed) {
try {
return supplier.get();
} catch (Throwable e) {
throw Exceptions.unchecked(e);
}
}
throw new RateLimiterException(key, max, ttl, timeUnit);
}
}
@@ -0,0 +1,52 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: DreamLu (596392912@qq.com)
*/
package org.springblade.core.redis.ratelimiter;
import lombok.Getter;
import java.util.concurrent.TimeUnit;
/**
* 限流异常
*
* @author L.cm
*/
@Getter
public class RateLimiterException extends RuntimeException {
private final String key;
private final long max;
private final long ttl;
private final TimeUnit timeUnit;
public RateLimiterException(String key, long max, long ttl, TimeUnit timeUnit) {
super(String.format("您的访问次数已超限:%s,速率:%d/%ds", key, max, timeUnit.toSeconds(ttl)));
this.key = key;
this.max = max;
this.ttl = ttl;
this.timeUnit = timeUnit;
}
}
@@ -0,0 +1,111 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: DreamLu (596392912@qq.com)
*/
package org.springblade.core.redis.ratelimiter;
import lombok.RequiredArgsConstructor;
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.springblade.core.tool.spel.BladeExpressionEvaluator;
import org.springblade.core.tool.utils.CharPool;
import org.springblade.core.tool.utils.StringUtil;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.expression.AnnotatedElementKey;
import org.springframework.expression.EvaluationContext;
import org.springframework.lang.NonNull;
import org.springframework.util.Assert;
import java.lang.reflect.Method;
import java.util.concurrent.TimeUnit;
/**
* redis 限流
*
* @author L.cm
*/
@Aspect
@RequiredArgsConstructor
public class RedisRateLimiterAspect implements ApplicationContextAware {
/**
* 表达式处理
*/
private final BladeExpressionEvaluator evaluator = new BladeExpressionEvaluator();
/**
* redis 限流服务
*/
private final RedisRateLimiterClient rateLimiterClient;
private ApplicationContext applicationContext;
/**
* AOP 环切 注解 @RateLimiter
*/
@Around("@annotation(limiter)")
public Object aroundRateLimiter(ProceedingJoinPoint point, RateLimiter limiter) throws Throwable {
String limitKey = limiter.value();
Assert.hasText(limitKey, "@RateLimiter value must have length; it must not be null or empty");
// el 表达式
String limitParam = limiter.param();
// 表达式不为空
String rateKey;
if (StringUtil.isNotBlank(limitParam)) {
String evalAsText = evalLimitParam(point, limitParam);
rateKey = limitKey + CharPool.COLON + evalAsText;
} else {
rateKey = limitKey;
}
long max = limiter.max();
long ttl = limiter.ttl();
TimeUnit timeUnit = limiter.timeUnit();
return rateLimiterClient.allow(rateKey, max, ttl, timeUnit, point::proceed);
}
/**
* 计算参数表达式
*
* @param point ProceedingJoinPoint
* @param limitParam limitParam
* @return 结果
*/
private String evalLimitParam(ProceedingJoinPoint point, String limitParam) {
MethodSignature ms = (MethodSignature) point.getSignature();
Method method = ms.getMethod();
Object[] args = point.getArgs();
Object target = point.getTarget();
Class<?> targetClass = target.getClass();
EvaluationContext context = evaluator.createContext(method, args, target, targetClass, applicationContext);
AnnotatedElementKey elementKey = new AnnotatedElementKey(method, targetClass);
return evaluator.evalAsText(limitParam, elementKey, context);
}
@Override
public void setApplicationContext(@NonNull ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
}
@@ -0,0 +1,87 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: DreamLu (596392912@qq.com)
*/
package org.springblade.core.redis.ratelimiter;
import lombok.RequiredArgsConstructor;
import org.springblade.core.tool.utils.CharPool;
import org.springframework.core.env.Environment;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.RedisScript;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
/**
* redis 限流服务
*
* @author dream.lu
*/
@RequiredArgsConstructor
public class RedisRateLimiterClient implements RateLimiterClient {
/**
* redis 限流 key 前缀
*/
private static final String REDIS_KEY_PREFIX = "limiter:";
/**
* 失败的默认返回值
*/
private static final long FAIL_CODE = 0;
/**
* redisTemplate
*/
private final StringRedisTemplate redisTemplate;
/**
* redisScript
*/
private final RedisScript<Long> script;
/**
* env
*/
private final Environment environment;
@Override
public boolean isAllowed(String key, long max, long ttl, TimeUnit timeUnit) {
// redis key
String redisKeyBuilder = REDIS_KEY_PREFIX +
getApplicationName(environment) + CharPool.COLON + key;
List<String> keys = Collections.singletonList(redisKeyBuilder);
// 转为毫秒
long ttlMillis = timeUnit.toMillis(ttl);
// 唯一成员: 保证同一时刻的多次请求在有序集合中各占一个成员, 避免被去重而漏计 (单 key 设计, 集群安全)
String member = UUID.randomUUID().toString();
// 执行命令
Long result = this.redisTemplate.execute(this.script, keys, Long.toString(max), Long.toString(ttlMillis), member);
return result != FAIL_CODE;
}
private static String getApplicationName(Environment environment) {
return environment.getProperty("spring.application.name", "");
}
}
@@ -0,0 +1,61 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: DreamLu (596392912@qq.com)
*/
package org.springblade.core.redis.serializer;
/**
* redis序列化辅助类.单纯的泛型无法定义通用schema,原因是无法通过泛型T得到Class
*
* @author L.cm
*/
public class BytesWrapper<T> implements Cloneable {
private T value;
public BytesWrapper() {
}
public BytesWrapper(T value) {
this.value = value;
}
public void setValue(T value) {
this.value = value;
}
public T getValue() {
return value;
}
@Override
@SuppressWarnings("unchecked")
public BytesWrapper<T> clone() {
try {
return (BytesWrapper) super.clone();
} catch (CloneNotSupportedException e) {
return new BytesWrapper<>();
}
}
}
@@ -0,0 +1,71 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: DreamLu (596392912@qq.com)
*/
package org.springblade.core.redis.serializer;
import io.protostuff.LinkedBuffer;
import io.protostuff.ProtobufIOUtil;
import io.protostuff.Schema;
import io.protostuff.runtime.RuntimeSchema;
import org.springblade.core.tool.utils.ObjectUtil;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.SerializationException;
/**
* ProtoStuff 序列化
*
* @author L.cm
*/
public class ProtoStuffSerializer implements RedisSerializer<Object> {
private final Schema<BytesWrapper> schema;
public ProtoStuffSerializer() {
this.schema = RuntimeSchema.getSchema(BytesWrapper.class);
}
@Override
public byte[] serialize(Object object) throws SerializationException {
if (object == null) {
return null;
}
LinkedBuffer buffer = LinkedBuffer.allocate(LinkedBuffer.DEFAULT_BUFFER_SIZE);
try {
return ProtobufIOUtil.toByteArray(new BytesWrapper<>(object), schema, buffer);
} finally {
buffer.clear();
}
}
@Override
public Object deserialize(byte[] bytes) throws SerializationException {
if (ObjectUtil.isEmpty(bytes)) {
return null;
}
BytesWrapper<Object> wrapper = new BytesWrapper<>();
ProtobufIOUtil.mergeFrom(bytes, wrapper, schema);
return wrapper.getValue();
}
}
@@ -0,0 +1,84 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: DreamLu (596392912@qq.com)
*/
package org.springblade.core.redis.serializer;
import org.springframework.cache.interceptor.SimpleKey;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.data.redis.serializer.RedisSerializer;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Objects;
/**
* 将redis key序列化为字符串
*
* <p>
* spring cache中的简单基本类型直接使用 StringRedisSerializer 会有问题
* </p>
*
* @author L.cm
*/
public class RedisKeySerializer implements RedisSerializer<Object> {
private final Charset charset;
private final ConversionService converter;
public RedisKeySerializer() {
this(StandardCharsets.UTF_8);
}
public RedisKeySerializer(Charset charset) {
Objects.requireNonNull(charset, "Charset must not be null");
this.charset = charset;
this.converter = DefaultConversionService.getSharedInstance();
}
@Override
public Object deserialize(byte[] bytes) {
// redis keys 会用到反序列化
if (bytes == null) {
return null;
}
return new String(bytes, charset);
}
@Override
public byte[] serialize(Object object) {
Objects.requireNonNull(object, "redis key is null");
String key;
if (object instanceof SimpleKey) {
key = "";
} else if (object instanceof String) {
key = (String) object;
} else {
key = converter.convert(object, String.class);
}
return key.getBytes(this.charset);
}
}
@@ -0,0 +1,122 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: DreamLu (596392912@qq.com)
*/
package org.springblade.core.redis.stream;
import org.springframework.data.redis.connection.RedisStreamCommands;
import org.springframework.data.redis.connection.stream.MapRecord;
import org.springframework.data.redis.connection.stream.Record;
import org.springframework.data.redis.connection.stream.RecordId;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StreamOperations;
import org.springframework.data.redis.core.convert.RedisCustomConversions;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
/**
* 默认的 RStreamTemplate
*
* @author L.cm
*/
public class DefaultRStreamTemplate implements RStreamTemplate {
private static final RedisCustomConversions CUSTOM_CONVERSIONS = new RedisCustomConversions();
private final RedisTemplate<String, Object> redisTemplate;
private final StreamOperations<String, String, Object> streamOperations;
public DefaultRStreamTemplate(RedisTemplate<String, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
this.streamOperations = redisTemplate.opsForStream();
}
@Override
public RecordId send(Record<String, ?> record) {
// 1. MapRecord
if (record instanceof MapRecord) {
return streamOperations.add(record);
}
String stream = Objects.requireNonNull(record.getStream(), "RStreamTemplate send stream name is null.");
Object recordValue = Objects.requireNonNull(record.getValue(), "RStreamTemplate send stream: " + stream + " value is null.");
Class<?> valueClass = recordValue.getClass();
// 2. 普通类型的 ObjectRecord
if (CUSTOM_CONVERSIONS.isSimpleType(valueClass)) {
return streamOperations.add(record);
}
// 3. 自定义类型处理
Map<String, Object> payload = new HashMap<>();
payload.put(RStreamTemplate.OBJECT_PAYLOAD_KEY, recordValue);
MapRecord<String, String, Object> mapRecord = MapRecord.create(stream, payload);
return streamOperations.add(mapRecord);
}
@Override
public RecordId send(String name, String key, byte[] data, RedisStreamCommands.XAddOptions options) {
RedisSerializer<String> stringSerializer = StringRedisSerializer.UTF_8;
byte[] nameBytes = Objects.requireNonNull(stringSerializer.serialize(name), "redis stream name is null.");
byte[] keyBytes = Objects.requireNonNull(stringSerializer.serialize(key), "redis stream key is null.");
Map<byte[], byte[]> mapDate = Collections.singletonMap(keyBytes, data);
return redisTemplate.execute((RedisCallback<RecordId>) redis -> {
RedisStreamCommands streamCommands = redis.streamCommands();
return streamCommands.xAdd(MapRecord.create(nameBytes, mapDate), options);
});
}
@Override
public Long delete(String name, String... recordIds) {
return streamOperations.delete(name, recordIds);
}
@Override
public Long delete(String name, RecordId... recordIds) {
return streamOperations.delete(name, recordIds);
}
@Override
public Long trim(String name, long count, boolean approximateTrimming) {
return streamOperations.trim(name, count, approximateTrimming);
}
@Override
public Long acknowledge(String name, String group, String... recordIds) {
return streamOperations.acknowledge(name, group, recordIds);
}
@Override
public Long acknowledge(String name, String group, RecordId... recordIds) {
return streamOperations.acknowledge(name, group, recordIds);
}
@Override
public Long acknowledge(String group, Record<String, ?> record) {
return streamOperations.acknowledge(group, record);
}
}
@@ -0,0 +1,46 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: DreamLu (596392912@qq.com)
*/
package org.springblade.core.redis.stream;
/**
* 消息类型
*
* @author L.cm
*/
public enum MessageModel {
/**
* 广播
*/
BROADCASTING,
/**
* 集群消息
*/
CLUSTERING;
}
@@ -0,0 +1,90 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: DreamLu (596392912@qq.com)
*/
package org.springblade.core.redis.stream;
import java.lang.annotation.*;
/**
* 基于 redis 的 stream 监听
*
* @author L.cm
*/
@Documented
@Inherited
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RStreamListener {
/**
* Queue name
*
* @return String
*/
String name();
/**
* consumer group,默认为服务名 + 环境
*
* @return String
*/
String group() default "";
/**
* 消息方式,集群模式和广播模式,如果想让所有订阅者收到所有消息,广播是一个不错的选择。
*
* @return MessageModel
*/
MessageModel messageModel() default MessageModel.CLUSTERING;
/**
* offsetModel,默认:LAST_CONSUMED
*
* <p>
* 0-0 : 从开始的地方读。
* $ :表示从尾部开始消费,只接受新消息,当前 Stream 消息会全部忽略。
* > : 读取所有新到达的元素,这些元素的id大于消费组使用的最后一个元素。
* </p>
*
* @return ReadOffsetModel
*/
ReadOffsetModel offsetModel() default ReadOffsetModel.LAST_CONSUMED;
/**
* 自动 ack
*
* @return boolean
*/
boolean autoAcknowledge() default false;
/**
* 读取原始的 bytes 数据
*
* @return boolean
*/
boolean readRawBytes() default false;
}
@@ -0,0 +1,169 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: DreamLu (596392912@qq.com)
*/
package org.springblade.core.redis.stream;
import lombok.extern.slf4j.Slf4j;
import org.springblade.core.tool.utils.ReflectUtil;
import org.springblade.core.tool.utils.StringUtil;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.data.redis.RedisSystemException;
import org.springframework.data.redis.connection.stream.*;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StreamOperations;
import org.springframework.data.redis.stream.StreamMessageListenerContainer;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ReflectionUtils;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.Map;
/**
* Redisson 监听器
*
* @author L.cm
*/
@Slf4j
public class RStreamListenerDetector implements BeanPostProcessor, InitializingBean {
private final StreamMessageListenerContainer<String, MapRecord<String, String, byte[]>> streamMessageListenerContainer;
private final RedisTemplate<String, Object> redisTemplate;
private final String consumerGroup;
private final String consumerName;
public RStreamListenerDetector(StreamMessageListenerContainer<String, MapRecord<String, String, byte[]>> streamMessageListenerContainer,
RedisTemplate<String, Object> redisTemplate, String consumerGroup, String consumerName) {
this.streamMessageListenerContainer = streamMessageListenerContainer;
this.redisTemplate = redisTemplate;
this.consumerGroup = consumerGroup;
this.consumerName = consumerName;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
Class<?> userClass = ClassUtils.getUserClass(bean);
ReflectionUtils.doWithMethods(userClass, method -> {
RStreamListener listener = AnnotationUtils.findAnnotation(method, RStreamListener.class);
if (listener != null) {
String streamKey = listener.name();
Assert.hasText(streamKey, "@RStreamListener name must not be empty.");
log.info("Found @RStreamListener on bean:{} method:{}", beanName, method);
// 校验 methodmethod 入参数大于等于1
int paramCount = method.getParameterCount();
if (paramCount > 1) {
throw new IllegalArgumentException("@RStreamListener on method " + method + " parameter count must less or equal to 1.");
}
// streamOffset
ReadOffset readOffset = listener.offsetModel().getReadOffset();
StreamOffset<String> streamOffset = StreamOffset.create(streamKey, readOffset);
// 消费模式
MessageModel messageModel = listener.messageModel();
if (MessageModel.BROADCASTING == messageModel) {
broadCast(streamOffset, bean, method, listener.readRawBytes());
} else {
String groupId = StringUtil.isNotBlank(listener.group()) ? listener.group() : consumerGroup;
Consumer consumer = Consumer.from(groupId, consumerName);
// 如果需要,创建 group
createGroupIfNeed(redisTemplate, streamKey, readOffset, groupId);
cluster(consumer, streamOffset, listener, bean, method);
}
}
}, ReflectionUtils.USER_DECLARED_METHODS);
return bean;
}
private void broadCast(StreamOffset<String> streamOffset, Object bean, Method method, boolean isReadRawBytes) {
streamMessageListenerContainer.receive(streamOffset, (message) -> {
// MapBackedRecord
invokeMethod(bean, method, message, isReadRawBytes);
});
}
private void cluster(Consumer consumer, StreamOffset<String> streamOffset, RStreamListener listener, Object bean, Method method) {
boolean autoAcknowledge = listener.autoAcknowledge();
StreamMessageListenerContainer.ConsumerStreamReadRequest<String> readRequest = StreamMessageListenerContainer.StreamReadRequest.builder(streamOffset).consumer(consumer).autoAcknowledge(autoAcknowledge).build();
StreamOperations<String, Object, Object> opsForStream = redisTemplate.opsForStream();
streamMessageListenerContainer.register(readRequest, (message) -> {
// MapBackedRecord
invokeMethod(bean, method, message, listener.readRawBytes());
// ack
if (autoAcknowledge) {
opsForStream.acknowledge(consumer.getGroup(), message);
}
});
}
private static void createGroupIfNeed(RedisTemplate<String, Object> redisTemplate, String streamKey, ReadOffset readOffset, String group) {
StreamOperations<String, Object, Object> opsForStream = redisTemplate.opsForStream();
try {
StreamInfo.XInfoGroups groups = opsForStream.groups(streamKey);
if (groups.stream().noneMatch((x) -> group.equals(x.groupName()))) {
opsForStream.createGroup(streamKey, readOffset, group);
}
} catch (RedisSystemException e) {
// RedisCommandExecutionException: ERR no such key
opsForStream.createGroup(streamKey, group);
}
}
private void invokeMethod(Object bean, Method method, MapRecord<String, String, byte[]> mapRecord, boolean isReadRawBytes) {
// 支持没有参数的方法
if (method.getParameterCount() == 0) {
ReflectUtil.invokeMethod(method, bean);
return;
}
if (isReadRawBytes) {
ReflectUtil.invokeMethod(method, bean, mapRecord);
} else {
ReflectUtil.invokeMethod(method, bean, getRecordValue(mapRecord));
}
}
private Object getRecordValue(MapRecord<String, String, byte[]> mapRecord) {
Map<String, byte[]> messageValue = mapRecord.getValue();
if (messageValue.containsKey(RStreamTemplate.OBJECT_PAYLOAD_KEY)) {
byte[] payloads = messageValue.get(RStreamTemplate.OBJECT_PAYLOAD_KEY);
Object deserialize = redisTemplate.getValueSerializer().deserialize(payloads);
return ObjectRecord.create(mapRecord.getStream(), deserialize).withId(mapRecord.getId());
} else {
return mapRecord.mapEntries(entry -> {
String key = entry.getKey();
Object value = redisTemplate.getValueSerializer().deserialize(entry.getValue());
return Collections.singletonMap(key, value).entrySet().iterator().next();
});
}
}
@Override
public void afterPropertiesSet() throws Exception {
streamMessageListenerContainer.start();
}
}
@@ -0,0 +1,270 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: DreamLu (596392912@qq.com)
*/
package org.springblade.core.redis.stream;
import org.springframework.data.redis.connection.RedisStreamCommands;
import org.springframework.data.redis.connection.stream.MapRecord;
import org.springframework.data.redis.connection.stream.ObjectRecord;
import org.springframework.data.redis.connection.stream.Record;
import org.springframework.data.redis.connection.stream.RecordId;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.lang.Nullable;
import java.util.Collections;
import java.util.Map;
import java.util.function.Function;
/**
* 基于 redis Stream 的消息发布器
*
* @author L.cm
*/
public interface RStreamTemplate {
/**
* 自定义 pojo 类型 key
*/
String OBJECT_PAYLOAD_KEY = "@payload";
/**
* 方便多 redis 数据源使用
*
* @param redisTemplate RedisTemplate
* @return MicaRedisCache
*/
static RStreamTemplate use(RedisTemplate<String, Object> redisTemplate) {
return new DefaultRStreamTemplate(redisTemplate);
}
/**
* 发布消息
*
* @param name 队列名
* @param value 消息
* @return 消息id
*/
default RecordId send(String name, Object value) {
return send(ObjectRecord.create(name, value));
}
/**
* 发布消息
*
* @param name 队列名
* @param key 消息key
* @param value 消息
* @return 消息id
*/
default RecordId send(String name, String key, Object value) {
return send(name, Collections.singletonMap(key, value));
}
/**
* 发布消息
*
* @param name 队列名
* @param key 消息key
* @param data 消息
* @return 消息id
*/
default RecordId send(String name, String key, byte[] data) {
return send(name, key, data, RedisStreamCommands.XAddOptions.none());
}
/**
* 发布消息
*
* @param name 队列名
* @param key 消息key
* @param data 消息
* @param maxLen 限制 stream 最大长度
* @return 消息id
*/
default RecordId send(String name, String key, byte[] data, long maxLen) {
return send(name, key, data, RedisStreamCommands.XAddOptions.maxlen(maxLen));
}
/**
* 发布消息
*
* @param name 队列名
* @param key 消息key
* @param data 消息
* @param options XAddOptions
* @return 消息id
*/
RecordId send(String name, String key, byte[] data, RedisStreamCommands.XAddOptions options);
/**
* 发布消息
*
* @param name 队列名
* @param key 消息key
* @param data 消息
* @param mapper mapper
* @param <T> 泛型
* @return 消息id
*/
default <T> RecordId send(String name, String key, T data, Function<T, byte[]> mapper, long maxLen) {
return send(name, key, mapper.apply(data), maxLen);
}
/**
* 发布消息
*
* @param name 队列名
* @param key 消息key
* @param data 消息
* @param mapper mapper
* @param options XAddOptions
* @param <T> 泛型
* @return 消息id
*/
default <T> RecordId send(String name, String key, T data, Function<T, byte[]> mapper, RedisStreamCommands.XAddOptions options) {
return send(name, key, mapper.apply(data), options);
}
/**
* 发布消息
*
* @param name 队列名
* @param key 消息key
* @param data 消息
* @param mapper 消息转换
* @param <T> 泛型
* @return 消息id
*/
default <T> RecordId send(String name, String key, T data, Function<T, byte[]> mapper) {
return send(name, key, mapper.apply(data));
}
/**
* 批量发布
*
* @param name 队列名
* @param messages 消息
* @return 消息id
*/
default RecordId send(String name, Map<String, Object> messages) {
return send(MapRecord.create(name, messages));
}
/**
* 发送消息
*
* @param record Record
* @return 消息id
*/
RecordId send(Record<String, ?> record);
/**
* 删除消息
*
* @param name stream name
* @param recordIds recordIds
* @return Long
*/
@Nullable
Long delete(String name, String... recordIds);
/**
* 删除消息
*
* @param name stream name
* @param recordIds recordIds
* @return Long
*/
@Nullable
Long delete(String name, RecordId... recordIds);
/**
* 删除消息
*
* @param record Record
* @return Long
*/
@Nullable
default Long delete(Record<String, ?> record) {
return delete(record.getStream(), record.getId());
}
/**
* 对流进行修剪,限制长度
*
* @param name name
* @param count count
* @return Long
*/
@Nullable
default Long trim(String name, long count) {
return trim(name, count, false);
}
/**
* 对流进行修剪,限制长度
*
* @param name name
* @param count count
* @param approximateTrimming approximateTrimming
* @return Long
*/
@Nullable
Long trim(String name, long count, boolean approximateTrimming);
/**
* 手动 ack
*
* @param name name
* @param group group
* @param recordIds recordIds
* @return Long
*/
@Nullable
Long acknowledge(String name, String group, String... recordIds);
/**
* 手动 ack
*
* @param name name
* @param group group
* @param recordIds recordIds
* @return Long
*/
@Nullable
Long acknowledge(String name, String group, RecordId... recordIds);
/**
* 手动 ack
*
* @param group group
* @param record record
* @return Long
*/
@Nullable
Long acknowledge(String group, Record<String, ?> record);
}
@@ -0,0 +1,60 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: DreamLu (596392912@qq.com)
*/
package org.springblade.core.redis.stream;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.connection.stream.ReadOffset;
/**
* stream read offset model
*
* @author L.cm
*/
@Getter
@RequiredArgsConstructor
public enum ReadOffsetModel {
/**
* 从开始的地方读
*/
START(ReadOffset.from("0-0")),
/**
* 从最近的偏移量读取。
*/
LATEST(ReadOffset.latest()),
/**
* 读取所有新到达的元素,这些元素的id大于最后一个消费组的id。
*/
LAST_CONSUMED(ReadOffset.lastConsumed());
/**
* readOffset
*/
private final ReadOffset readOffset;
}
@@ -0,0 +1,42 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: DreamLu (596392912@qq.com)
*/
package org.springblade.core.redis.support;
/**
* redis常量
* <a href="https://redis.io/commands/bitcount/#History">redis 版本 7.0以上</a>
*/
public enum BitMapModel {
/**
* BYTE
*/
BYTE,
/**
* BIT
*/
BIT,
}
@@ -0,0 +1,26 @@
-- 开启单命令(效果)复制模式: 脚本内使用了 TIME 等非确定性命令, 需按写效果复制以保证主从/AOF 一致
redis.replicate_commands()
-- 周期内允许的最大请求数
local max = tonumber(ARGV[1])
-- 时间窗口大小, 单位毫秒
local ttl = tonumber(ARGV[2])
-- 唯一成员, 由客户端生成(每次请求唯一), 保证同一时刻的多次请求各计一次, 避免有序集合成员去重
local member = ARGV[3]
-- 当前时间, 毫秒。TIME 返回 {秒, 微秒}, 统一换算为毫秒, 与 ttl 单位对齐(修正历史秒/毫秒混用导致滑窗失效的问题)
local time = redis.call('TIME')
local now = time[1] * 1000 + math.floor(time[2] / 1000)
-- 滑动窗口: 清除窗口左边界(now - ttl)之前的历史请求
redis.call('zremrangebyscore', KEYS[1], 0, now - ttl)
-- 窗口内当前请求数(成员唯一, 此值即真实请求数)
local currentLimit = tonumber(redis.call('zcard', KEYS[1]))
local nextLimit = currentLimit + 1
if nextLimit > max then
-- 达到限流阈值, 拒绝
return 0
else
-- 未达阈值: score 记毫秒时间戳(供滑窗裁剪), member 记唯一值(供精确计数)
redis.call('zadd', KEYS[1], now, member)
-- 刷新整个 key 的过期时间, 兜底回收空闲 key
redis.call('pexpire', KEYS[1], ttl)
return nextLimit
end
@@ -0,0 +1,11 @@
{
"properties": [
{
"name": "blade.redis.rate-limiter.enabled",
"type": "java.lang.Boolean",
"description": "是否开启 redis 分布式限流.",
"defaultValue": "false"
}
]
}