init
This commit is contained in:
@@ -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>
|
||||
+1138
File diff suppressed because it is too large
Load Diff
+59
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
+80
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
+124
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+95
@@ -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;
|
||||
}
|
||||
}
|
||||
+75
@@ -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);
|
||||
}
|
||||
}
|
||||
+55
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
+73
@@ -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);
|
||||
}
|
||||
}
|
||||
+73
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
+52
@@ -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());
|
||||
}
|
||||
}
|
||||
+77
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
+140
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
+96
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
+60
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
+82
@@ -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);
|
||||
|
||||
}
|
||||
+85
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
+197
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
+100
@@ -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 "操作过于频繁,请稍后再试";
|
||||
|
||||
}
|
||||
+160
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
+102
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
+64
@@ -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;
|
||||
|
||||
}
|
||||
+161
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
+114
@@ -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;
|
||||
}
|
||||
+112
@@ -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;
|
||||
}
|
||||
}
|
||||
+100
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
+88
@@ -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;
|
||||
}
|
||||
+49
@@ -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();
|
||||
|
||||
}
|
||||
+91
@@ -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);
|
||||
|
||||
// 校验 method,method 入参数大于等于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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+63
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
+45
@@ -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);
|
||||
|
||||
}
|
||||
+55
@@ -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.");
|
||||
}
|
||||
}
|
||||
+76
@@ -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;
|
||||
}
|
||||
+100
@@ -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);
|
||||
}
|
||||
}
|
||||
+52
@@ -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;
|
||||
}
|
||||
}
|
||||
+111
@@ -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;
|
||||
}
|
||||
}
|
||||
+87
@@ -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", "");
|
||||
}
|
||||
|
||||
}
|
||||
+61
@@ -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<>();
|
||||
}
|
||||
}
|
||||
}
|
||||
+71
@@ -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();
|
||||
}
|
||||
}
|
||||
+84
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
+122
@@ -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;
|
||||
|
||||
}
|
||||
+90
@@ -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;
|
||||
|
||||
}
|
||||
+169
@@ -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);
|
||||
// 校验 method,method 入参数大于等于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();
|
||||
}
|
||||
|
||||
}
|
||||
+270
@@ -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);
|
||||
|
||||
}
|
||||
+60
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user