diff --git a/magic-api-plugins/magic-api-plugin-dynamic-redis/pom.xml b/magic-api-plugins/magic-api-plugin-dynamic-redis/pom.xml new file mode 100644 index 00000000..dbcd9922 --- /dev/null +++ b/magic-api-plugins/magic-api-plugin-dynamic-redis/pom.xml @@ -0,0 +1,25 @@ + + + 4.0.0 + + org.ssssssss + magic-api-plugins + 3.0.0 + + magic-api-plugin-dynamic-redis + jar + magic-api-plugin-dynamic-redis + magic-api-plugin-dynamic-redis + + + org.springframework.boot + spring-boot-starter-data-redis + + + org.apache.commons + commons-pool2 + + + diff --git a/magic-api-plugins/magic-api-plugin-dynamic-redis/src/main/java/org/ssssssss/magicapi/redis/MagicDynamicRedisAutoConfig.java b/magic-api-plugins/magic-api-plugin-dynamic-redis/src/main/java/org/ssssssss/magicapi/redis/MagicDynamicRedisAutoConfig.java new file mode 100644 index 00000000..54d244d2 --- /dev/null +++ b/magic-api-plugins/magic-api-plugin-dynamic-redis/src/main/java/org/ssssssss/magicapi/redis/MagicDynamicRedisAutoConfig.java @@ -0,0 +1,76 @@ +package org.ssssssss.magicapi.redis; + + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.DependsOn; +import org.springframework.context.annotation.Import; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.scheduling.annotation.EnableAsync; +import org.ssssssss.magicapi.utils.Assert; + +import java.util.LinkedHashMap; +import java.util.Map; + + +@Slf4j +@EnableAsync +@Configuration +@DependsOn("redisInstanceFactory") +@ConfigurationProperties(prefix = "magic-api.redis") +@Import({MagicRedisConfiguration.class,RedisInstanceFactory.class,MagicRedisConfiguration.class}) +public class MagicDynamicRedisAutoConfig implements InitializingBean { + + @Getter + public static String primary; + + @Getter + private static Map dynamic = new LinkedHashMap<>(); + + @Override + public void afterPropertiesSet() { + Assert.isNotNull(primary, "Redis未设置默认库"); + dynamic.forEach((redisName, redisProperty) -> { + // 构建Redis服务 + RedisInstanceFactory.getInstance().buildRedisTemplate(redisName, redisProperty); + }); + log.info("[动态Redis]--共创建Redis[{}]个", dynamic.size()); + } + + @Bean + public StringRedisTemplate stringRedisTemplate() { + MagicRedisProperties redisProperties = dynamic.get(primary); + RedisConnectionFactory connectionFactory = RedisInstanceFactory.getInstance().buildLettuceConnectionFactory(primary, redisProperties); + StringRedisTemplate template = new StringRedisTemplate(); + template.setConnectionFactory(connectionFactory); + return template; + } + + @Bean + public RedisTemplate redisTemplate() { + MagicRedisProperties redisProperties = dynamic.get(primary); + RedisConnectionFactory connectionFactory = RedisInstanceFactory.getInstance().buildLettuceConnectionFactory(primary, redisProperties); + RedisTemplate template = new RedisTemplate<>(); + template.setConnectionFactory(connectionFactory); + return template; + } + + + public void setPrimary(String primary) { + MagicDynamicRedisAutoConfig.primary = primary; + } + + public void setDynamic(Map dynamic) { + MagicDynamicRedisAutoConfig.dynamic = dynamic; + } + + public static MagicRedisProperties getPrimaryRedisProp() { + return dynamic.get(primary); + } +} diff --git a/magic-api-plugins/magic-api-plugin-dynamic-redis/src/main/java/org/ssssssss/magicapi/redis/MagicRedisConfiguration.java b/magic-api-plugins/magic-api-plugin-dynamic-redis/src/main/java/org/ssssssss/magicapi/redis/MagicRedisConfiguration.java new file mode 100644 index 00000000..2c6309de --- /dev/null +++ b/magic-api-plugins/magic-api-plugin-dynamic-redis/src/main/java/org/ssssssss/magicapi/redis/MagicRedisConfiguration.java @@ -0,0 +1,50 @@ +package org.ssssssss.magicapi.redis; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.ssssssss.magicapi.core.config.MagicAPIProperties; +import org.ssssssss.magicapi.core.config.MagicPluginConfiguration; +import org.ssssssss.magicapi.core.config.Resource; +import org.ssssssss.magicapi.core.model.Plugin; +/** + * MagicRedisProperties + * @since 3.0.0 + * @author 冰点 + */ +@Configuration +public class MagicRedisConfiguration implements MagicPluginConfiguration { + + private final MagicAPIProperties properties; + + public MagicRedisConfiguration(MagicAPIProperties properties) { + this.properties = properties; + } + + /** + * 使用Redis存储 + */ + @Bean + @ConditionalOnMissingBean + @ConditionalOnProperty(prefix = "magic-api", name = "resource.type", havingValue = "redis") + public org.ssssssss.magicapi.core.resource.Resource magicRedisResource(RedisInstanceFactory redisInstanceFactory) { + Resource resource = properties.getResource(); + StringRedisTemplate redisTemplate = (StringRedisTemplate) redisInstanceFactory.getRedisTemplate(MagicDynamicRedisAutoConfig.getPrimary(), true); + return new RedisResource(redisTemplate, resource.getPrefix(), resource.isReadonly()); + } + + /** + * 注入redis模块 + */ + @Bean + public RedisDynamicModule redisFunctions(RedisInstanceFactory redisInstanceFactory) { + return new RedisDynamicModule(redisInstanceFactory); + } + + @Override + public Plugin plugin() { + return new Plugin("dynamicRedis"); + } +} diff --git a/magic-api-plugins/magic-api-plugin-dynamic-redis/src/main/java/org/ssssssss/magicapi/redis/MagicRedisProperties.java b/magic-api-plugins/magic-api-plugin-dynamic-redis/src/main/java/org/ssssssss/magicapi/redis/MagicRedisProperties.java new file mode 100644 index 00000000..b685caa2 --- /dev/null +++ b/magic-api-plugins/magic-api-plugin-dynamic-redis/src/main/java/org/ssssssss/magicapi/redis/MagicRedisProperties.java @@ -0,0 +1,130 @@ +package org.ssssssss.magicapi.redis; + + + +import lombok.Data; +import lombok.experimental.Accessors; +import lombok.extern.slf4j.Slf4j; + +import java.time.Duration; +/** + * MagicRedisProperties + * @since 3.0.0 + * @author 冰点 + */ + +@Slf4j +@Data +@Accessors(chain = true) +public class MagicRedisProperties { + + /** + * 连接类型 + */ + private RedisConnectionType type; + + /** + * redis主机地址,ip:port,多个用逗号(,)分隔 + */ + private String address = "localhost:6379"; + + /** + * 用户名 + */ + private String username; + + /** + * 登录密码 + */ + private String password; + + /** + * 数据库(默认0) + */ + private int database = 0; + + /** + * 连接超时时间,默认3S + */ + private Duration timeout = Duration.ofMillis(3000L); + + /** + * 哨兵时需要指定master Name + */ + private Sentinel sentinel = new Sentinel(); + + /** + * 默认使用lettuce + */ + private Lettuce lettuce = new Lettuce(); + + /** + * 重试次数 + */ + private int maxRedirects = 5; + + /** + * 哨兵模式 + */ + @Data + public static class Sentinel { + private String master; + } + + /** + * Lettuce client配置 + */ + @Data + public static class Lettuce { + + /** + * Shutdown timeout. + */ + private Duration shutdownTimeout = Duration.ofMillis(100); + + /** + * Lettuce pool configuration. + */ + private MagicRedisProperties.Pool pool = new Pool(); + + /** + * setShutdownTimeout + */ + public void setShutdownTimeout(long shutdownTimeout) { + this.shutdownTimeout = Duration.ofMillis(shutdownTimeout); + } + } + + /** + * 连接池属性 + */ + @Data + public static class Pool { + /** + * 最大空闲 + */ + private int maxIdle = 8; + + /** + * 最小空闲 + */ + private int minIdle = 0; + + /** + * 最大存活 + */ + private int maxActive = 8; + + /** + * 最长等待 + */ + private Duration maxWait = Duration.ofMillis(-1); + + /** + * long转Duration + */ + public void setMaxWait(long maxWait) { + this.maxWait = Duration.ofMillis(maxWait); + } + } +} diff --git a/magic-api-plugins/magic-api-plugin-dynamic-redis/src/main/java/org/ssssssss/magicapi/redis/RedisConnectionType.java b/magic-api-plugins/magic-api-plugin-dynamic-redis/src/main/java/org/ssssssss/magicapi/redis/RedisConnectionType.java new file mode 100644 index 00000000..de161462 --- /dev/null +++ b/magic-api-plugins/magic-api-plugin-dynamic-redis/src/main/java/org/ssssssss/magicapi/redis/RedisConnectionType.java @@ -0,0 +1,59 @@ +package org.ssssssss.magicapi.redis; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * + * @since 3.0.0 + * @author 冰点 + */ +@Getter +@AllArgsConstructor +public enum RedisConnectionType { + + /** + * 单机部署(默认) + */ + STANDALONE("standalone", "单机部署"), + + /** + * 哨兵部署 + */ + SENTINEL("sentinel", "哨兵部署"), + + /** + * 集群部署方式 + */ + CLUSTER("cluster", "集群方式"), + + /** + * 主从部署方式 + */ + MASTERSLAVE("masterslave", "主从部署"); + + /** + * 类型 + */ + private final String type; + /** + * 名称 + */ + private final String name; + + /** + * 根据type查询枚举 + * + * @param type 类型 + * @return 链接类型 + */ + public static RedisConnectionType match(String type) { + RedisConnectionType[] values = RedisConnectionType.values(); + for (RedisConnectionType connectionType : values) { + if (connectionType.type.equals(type)) { + return connectionType; + } + } + return null; + } +} diff --git a/magic-api-plugins/magic-api-plugin-dynamic-redis/src/main/java/org/ssssssss/magicapi/redis/RedisConstant.java b/magic-api-plugins/magic-api-plugin-dynamic-redis/src/main/java/org/ssssssss/magicapi/redis/RedisConstant.java new file mode 100644 index 00000000..d66a5aad --- /dev/null +++ b/magic-api-plugins/magic-api-plugin-dynamic-redis/src/main/java/org/ssssssss/magicapi/redis/RedisConstant.java @@ -0,0 +1,20 @@ +package org.ssssssss.magicapi.redis; + +/** + * + * @since 3.0.0 + * @author 冰点 + */ +public interface RedisConstant { + /** + * stringRedisTemplate 服务后缀名 + */ + String STRING_REDIS_TEMPLATE = "StringRedisTemplate"; + + /** + * RedisTemplate 服务后缀名 + */ + String REDIS_TEMPLATE = "RedisTemplate"; + + +} diff --git a/magic-api-plugins/magic-api-plugin-dynamic-redis/src/main/java/org/ssssssss/magicapi/redis/RedisDynamicModule.java b/magic-api-plugins/magic-api-plugin-dynamic-redis/src/main/java/org/ssssssss/magicapi/redis/RedisDynamicModule.java new file mode 100644 index 00000000..74b5513d --- /dev/null +++ b/magic-api-plugins/magic-api-plugin-dynamic-redis/src/main/java/org/ssssssss/magicapi/redis/RedisDynamicModule.java @@ -0,0 +1,146 @@ +package org.ssssssss.magicapi.redis; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.data.redis.connection.DefaultStringRedisConnection; +import org.springframework.data.redis.connection.RedisConnection; +import org.springframework.data.redis.connection.RedisPipelineException; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.util.ReflectionUtils; +import org.ssssssss.magicapi.core.annotation.MagicModule; +import org.ssssssss.magicapi.utils.Assert; +import org.ssssssss.script.functions.DynamicMethod; +import org.ssssssss.script.reflection.JavaReflection; + +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.*; + +/** + * redis 多数据源插件模块 + * @since 3.0.0 + * @author 冰点 + */ +@MagicModule("dynamicRedis") +@Slf4j +public class RedisDynamicModule implements DynamicMethod { + + private final RedisInstanceFactory redisInstanceFactory; + private RedisTemplate redisTemplate; + + private boolean isRedisson; + + public RedisDynamicModule(RedisInstanceFactory redisInstanceFactory) { + this.redisInstanceFactory = redisInstanceFactory; + } + + /** + * 序列化 + */ + public RedisDynamicModule name(String name) { + this.redisTemplate = redisInstanceFactory.getRedisTemplate(name, false); + Assert.isTrue(redisTemplate==null, "当前没有配置redis " + name); + this.isRedisson = Objects.equals("org.redisson.spring.data.connection.RedissonConnectionFactory", this.redisTemplate.getConnectionFactory().getClass().getName()); + return this; + } + + + /** + * 序列化 + */ + private byte[] serializer(Object value) { + if (value == null || value instanceof String) { + return redisTemplate.getStringSerializer().serialize((String) value); + } + return serializer(value.toString()); + } + + private Object serializerForRedisson(Object value) { + if (value == null || JavaReflection.isPrimitiveAssignableFrom(value.getClass(), value.getClass())) { + return value; + } + return serializer(value.toString()); + } + + /** + * 反序列化 + */ + @SuppressWarnings("unchecked") + private Object deserialize(Object value) { + if (value != null) { + if (value instanceof byte[]) { + return this.redisTemplate.getStringSerializer().deserialize((byte[]) value); + } + if (value instanceof List) { + List valueList = (List) value; + List resultList = new ArrayList<>(valueList.size()); + for (Object val : valueList) { + resultList.add(deserialize(val)); + } + return resultList; + } + if (value instanceof Map) { + Map map = (Map) value; + LinkedHashMap newMap = new LinkedHashMap<>(map.size()); + map.forEach((key, val) -> newMap.put(deserialize(key), deserialize(val))); + return newMap; + } + } + return value; + } + + /** + * 执行命令 + * + * @param methodName 命令名称 + * @param parameters 命令参数 + */ + @Override + public Object execute(String methodName, List parameters) { + return this.redisTemplate.execute(connection -> { + Object result; + if (isRedisson) { + result = executeForRedisson(((DefaultStringRedisConnection) connection).getDelegate(), methodName, parameters); + } else { + byte[][] params = new byte[parameters.size()][]; + for (int i = 0; i < params.length; i++) { + params[i] = serializer(parameters.get(i)); + } + result = connection.execute(methodName, params); + } + return deserialize(result); + }, isRedisson || this.redisTemplate.isExposeConnection()); + } + + private Object executeForRedisson(RedisConnection connection, String command, List parameters) { + Method[] methods = connection.getClass().getDeclaredMethods(); + for (Method method : methods) { + if (method.getName().equalsIgnoreCase(command) && Modifier.isPublic(method.getModifiers()) && method.getParameterTypes().length == parameters.size()) { + try { + Object ret = this.execute(connection, method, parameters); + if (ret instanceof String) { + return ((String) ret).getBytes(); + } + return ret; + } catch (IllegalArgumentException e) { + if (connection.isPipelined()) { + throw new RedisPipelineException(e); + } + + throw new InvalidDataAccessApiUsageException(e.getMessage(), e); + } + } + } + throw new UnsupportedOperationException(); + } + + private Object execute(RedisConnection connection, Method method, List parameters) { + if (method.getParameterTypes().length > 0 && method.getParameterTypes()[0] == byte[][].class) { + return ReflectionUtils.invokeMethod(method, connection, parameters.stream().map(this::serializer).toArray(byte[][]::new)); + } else if (parameters.size() == 0) { + return ReflectionUtils.invokeMethod(method, connection); + } + return ReflectionUtils.invokeMethod(method, connection, parameters.stream().map(this::serializerForRedisson).toArray()); + } + +} diff --git a/magic-api-plugins/magic-api-plugin-dynamic-redis/src/main/java/org/ssssssss/magicapi/redis/RedisInstanceFactory.java b/magic-api-plugins/magic-api-plugin-dynamic-redis/src/main/java/org/ssssssss/magicapi/redis/RedisInstanceFactory.java new file mode 100644 index 00000000..a386d144 --- /dev/null +++ b/magic-api-plugins/magic-api-plugin-dynamic-redis/src/main/java/org/ssssssss/magicapi/redis/RedisInstanceFactory.java @@ -0,0 +1,396 @@ +package org.ssssssss.magicapi.redis; + + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.PropertyAccessor; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.pool2.impl.GenericObjectPoolConfig; +import org.springframework.beans.BeansException; +import org.springframework.beans.MutablePropertyValues; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.ConstructorArgumentValues; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.support.GenericBeanDefinition; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.data.redis.connection.*; +import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration; +import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; +import org.springframework.data.redis.connection.lettuce.LettucePoolingClientConfiguration; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.StringRedisSerializer; +import org.springframework.stereotype.Component; +import org.ssssssss.magicapi.utils.Assert; +import org.springframework.util.StringUtils; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * redis 多数据源插件模块 + * @since 3.0.0 + * @author 冰点 + */ +@Slf4j +@Component +public class RedisInstanceFactory implements ApplicationContextAware { + + /** + * RedisTemplate缓存 取值规则 redisName + “RedisTemplate” 和 redisName + “StringRedisTemplate” + */ + public final Map> REDIS_HOLDER = new ConcurrentHashMap<>(); + + private static ApplicationContext applicationContext; + + private RedisInstanceFactory() { + } + + public static class RedisFactoryLoader { + private static final RedisInstanceFactory INSTANCE = new RedisInstanceFactory(); + } + + public static RedisInstanceFactory getInstance() { + return RedisFactoryLoader.INSTANCE; + } + + public RedisTemplate getRedisTemplate(String redisName, boolean isStringRedisTemplate) { + return REDIS_HOLDER.get(getRedisTemplateName(redisName, isStringRedisTemplate)); + } + /** + * 生成RedisTemplate服务 + * + * @param redisName redis名称 + * @param redisProperties redis属性 + */ + public void buildRedisTemplate(String redisName, MagicRedisProperties redisProperties) { + RedisConnectionFactory redisConnectionFactory = buildLettuceConnectionFactory(redisName, redisProperties); + Assert.isNotNull(redisConnectionFactory, "Redis连接创建失败"); + RedisTemplate redisTemplate = buildRedisTemplate(redisName, redisConnectionFactory); + Assert.isNotNull(redisTemplate, "RedisTemplate创建失败"); + StringRedisTemplate stringRedisTemplate = buildStringRedisTemplate(redisName, redisConnectionFactory); + Assert.isNotNull(stringRedisTemplate, "StringRedisTemplate创建失败"); + } + + /** + * 创建 ConnectionFactory + */ + public RedisConnectionFactory buildLettuceConnectionFactory(String redisName, MagicRedisProperties redisProperties) { + String redisClientName = getRedisTemplateName(redisName, false); + if (applicationContext.containsBean(redisClientName)) { + return getBean(redisClientName); + } + ConfigurableApplicationContext context = (ConfigurableApplicationContext) applicationContext; + DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory) context.getBeanFactory(); + // 获取连接工厂 + RedisConnectionFactory lettuceConnectionFactory = getLettuceConnectionFactory(redisProperties); + BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder + .genericBeanDefinition(RedisConnectionFactory.class, () -> lettuceConnectionFactory); + BeanDefinition beanDefinition = beanDefinitionBuilder.getRawBeanDefinition(); + // 设置primary bean + if (redisName.startsWith(MagicDynamicRedisAutoConfig.primary)) { + beanDefinition.setPrimary(true); + } + beanFactory.registerBeanDefinition(redisClientName, beanDefinition); + return getBean(redisClientName); + } + + /** + * 获取Lettuce连接工厂 + * + * @param redisProperties Redis配置 + */ + public static RedisConnectionFactory getLettuceConnectionFactory(MagicRedisProperties redisProperties) { + MagicRedisProperties.Pool pool = redisProperties.getLettuce().getPool(); + GenericObjectPoolConfig poolConfig = (new PoolBuilderFactory()).getPoolConfig(pool); + LettuceClientConfiguration clientConfig = LettucePoolingClientConfiguration.builder() + .commandTimeout(redisProperties.getTimeout()) + .poolConfig(poolConfig) + .build(); + LettuceConnectionFactory lettuceConnectionFactory = null; + RedisConnectionType connectionType = redisProperties.getType(); + if (RedisConnectionType.SENTINEL.equals(connectionType)) { + lettuceConnectionFactory = new LettuceConnectionFactory(buildSentinelConfig(redisProperties), clientConfig); + } else if (RedisConnectionType.CLUSTER.equals(connectionType)) { + lettuceConnectionFactory = new LettuceConnectionFactory(buildClusterConfig(redisProperties), clientConfig); + } else { + lettuceConnectionFactory = new LettuceConnectionFactory(buildStandaloneConfig(redisProperties), clientConfig); + } + return lettuceConnectionFactory; + } + + /** + * 生成 StringRedisTemplate + * + * @param redisName redis名称 + * @param lettuceConnectionFactory 连接工厂 + * @return StringRedisTemplate + */ + private StringRedisTemplate buildStringRedisTemplate(String redisName, RedisConnectionFactory lettuceConnectionFactory) { + ConstructorArgumentValues constructorArgumentValues = new ConstructorArgumentValues(); + constructorArgumentValues.addIndexedArgumentValue(0, lettuceConnectionFactory); + + String beanName = getRedisTemplateName(redisName, true); + registerBean(beanName, StringRedisTemplate.class,null, constructorArgumentValues); + StringRedisTemplate redisTemplate = getBean(beanName); + // 保存RedisTemplate + saveRedisBean(beanName, redisTemplate); + log.info("[动态Redis] - {}创建成功", beanName); + return redisTemplate; + } + + + /** + * 生成RedisTemplate + * + * @param redisName 缓存名称 + * @param connectionFactory 连接工厂 + * @return RedisTemplate + */ + private RedisTemplate buildRedisTemplate(String redisName, RedisConnectionFactory connectionFactory) { + String redisClientName = redisName + RedisConstant.REDIS_TEMPLATE; + if (applicationContext.containsBean(redisClientName)) { + return getBean(redisClientName); + } + ObjectMapper mapper = new ObjectMapper(); + mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); + mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); + Jackson2JsonRedisSerializer serializer = new Jackson2JsonRedisSerializer<>(mapper, Object.class); + StringRedisSerializer keySerializer = new StringRedisSerializer(); + + // 构造参数 + Map property = new HashMap<>(8); + property.put("connectionFactory", connectionFactory); + property.put("keySerializer", keySerializer); + property.put("hashKeySerializer", keySerializer); + property.put("valueSerializer", serializer); + property.put("hashValueSerializer", serializer); + registerBean(redisClientName, RedisTemplate.class, property,null); +// setBean(redisClientName, RedisTemplate.class, property); + RedisTemplate redisTemplate = getBean(redisClientName); + saveRedisBean(redisClientName, redisTemplate); + log.info("[动态Redis] - {}创建成功", redisClientName); + return redisTemplate; + } + + + /** + * 组装单机配置参数 + * + * @param redisProperties 属性配置 + * @return 构造Bean参数 + */ + public static RedisStandaloneConfiguration buildStandaloneConfig(MagicRedisProperties redisProperties) { + RedisStandaloneConfiguration standaloneConfig = new RedisStandaloneConfiguration(); + String[] addresses = redisProperties.getAddress().split(":"); + standaloneConfig.setHostName(addresses[0]); + standaloneConfig.setPort(Integer.parseInt(addresses[1])); + String username = redisProperties.getUsername(); + if (StringUtils.hasText(username)) { + standaloneConfig.setUsername(username); + } + String password = redisProperties.getPassword(); + if (StringUtils.hasText(password)) { + standaloneConfig.setPassword(RedisPassword.of(password)); + } + standaloneConfig.setDatabase(redisProperties.getDatabase()); + return standaloneConfig; + } + + + /** + * 组装哨兵配置参数 + * + * @param redisProperties 属性配置 + * @return 构造Bean参数 + */ + private static RedisSentinelConfiguration buildSentinelConfig(MagicRedisProperties redisProperties) { + String[] addresses = redisProperties.getAddress().split(","); + Assert.isTrue(addresses.length > 1, "哨兵模式不能配置单节点"); + List sentinelNode = new ArrayList<>(); + for (String address : addresses) { + Assert.isTrue(address.contains(","), "地址格式不正确,格式必须是:[地址:端口]"); + String[] addr = address.split(","); + sentinelNode.add(new RedisNode(addr[0].trim(), Integer.parseInt(addr[1]))); + } + String masterName = redisProperties.getSentinel().getMaster(); + Assert.isNotBlank(masterName, "哨兵模式必须指定主节点名称"); + RedisSentinelConfiguration sentinelConfig = new RedisSentinelConfiguration(); + sentinelConfig.setMaster(masterName); + sentinelConfig.setDatabase(redisProperties.getDatabase()); + String username = redisProperties.getUsername(); + if (StringUtils.hasText(username)) { + sentinelConfig.setUsername(username); + } + String password = redisProperties.getPassword(); + if (StringUtils.hasText(password)) { + sentinelConfig.setPassword(RedisPassword.of(password)); + } + sentinelConfig.setSentinels(sentinelNode); + return sentinelConfig; + } + + /** + * 组装集群配置参数 + * + * @param redisProperties 属性配置 + * @return 构造Bean参数 + */ + private static RedisClusterConfiguration buildClusterConfig(MagicRedisProperties redisProperties) { + String[] addresses = redisProperties.getAddress().split(","); + Assert.isTrue(addresses.length > 1, "集群模式不能配置单节点"); + // 集群节点 + List clusterNode = new ArrayList<>(); + for (String address : addresses) { + Assert.isTrue(address.contains(","), "地址格式不正确,格式必须是:[地址:端口]"); + String[] addr = address.split(","); + clusterNode.add(new RedisNode(addr[0].trim(), Integer.parseInt(addr[1]))); + } + RedisClusterConfiguration clusterConfig = new RedisClusterConfiguration(); + clusterConfig.setClusterNodes(clusterNode); + // 密码,处理带用户名的场景 + String username = redisProperties.getUsername(); + if (StringUtils.hasText(username)) { + clusterConfig.setUsername(username); + } + String password = redisProperties.getPassword(); + if (StringUtils.hasText(password)) { + clusterConfig.setPassword(RedisPassword.of(password)); + } + // 集群默认重试次数 + int maxRedirects = redisProperties.getMaxRedirects(); + clusterConfig.setMaxRedirects(maxRedirects <= 0 ? 5 : maxRedirects); + return clusterConfig; + } + + /** + * 注册Bean + * + * @param beanName bean名称 + * @param clazz class + * @param constructorArgs 构造函数 + */ + public synchronized void setCosBean(String beanName, Class clazz, ConstructorArgumentValues constructorArgs) { + checkApplicationContext(); + DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory) applicationContext.getAutowireCapableBeanFactory(); + if (beanFactory.containsBean(beanName)) { + return; + } + GenericBeanDefinition definition = new GenericBeanDefinition(); + //类class + definition.setBeanClass(clazz); + if (beanName.startsWith(MagicDynamicRedisAutoConfig.primary)) { + definition.setPrimary(true); + } + //属性赋值 + definition.setConstructorArgumentValues(new ConstructorArgumentValues(constructorArgs)); + //注册到spring上下文 + beanFactory.registerBeanDefinition(beanName, definition); + } + + /** + * 同步方法注册bean到ApplicationContext中 + * + * @param beanName bean name + * @param clazz bean class + * @param original bean的属性值 + */ + public synchronized void setBean(String beanName, Class clazz, Map original) { + checkApplicationContext(); + DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory) applicationContext.getAutowireCapableBeanFactory(); + if (beanFactory.containsBean(beanName)) { + return; + } + GenericBeanDefinition definition = new GenericBeanDefinition(); + //类class + definition.setBeanClass(clazz); + if (beanName.startsWith(MagicDynamicRedisAutoConfig.primary)) { + definition.setPrimary(true); + } + //属性赋值 + definition.setPropertyValues(new MutablePropertyValues(original)); + //注册到spring上下文 + beanFactory.registerBeanDefinition(beanName, definition); + } + + private String getRedisTemplateName(String redisName, boolean isStringRedisTemplate) { + return redisName + (isStringRedisTemplate ? RedisConstant.STRING_REDIS_TEMPLATE : RedisConstant.REDIS_TEMPLATE); + } + + private void registerBeanToContext(String beanName, Class clazz, Map properties, ConstructorArgumentValues args) { + checkApplicationContext(); + DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory) applicationContext.getAutowireCapableBeanFactory(); + if (beanFactory.containsBean(beanName)) { + return; + } + GenericBeanDefinition definition = new GenericBeanDefinition(); + definition.setBeanClass(clazz); + definition.setPrimary(beanName.startsWith(MagicDynamicRedisAutoConfig.primary)); + if (args != null) { + definition.setConstructorArgumentValues(args); + } else { + definition.setPropertyValues(new MutablePropertyValues(properties)); + } + beanFactory.registerBeanDefinition(beanName, definition); + } + + // 合并 setCosBean 和 setBean 方法 + public synchronized void registerBean(String beanName, Class clazz, Map properties, ConstructorArgumentValues args) { + registerBeanToContext(beanName, clazz, properties, args); + } + /** + * 保存RedisBean + * + * @param beanName redisName名称 + * @param redisTemplate redisTemplate + */ + private void saveRedisBean(String beanName, RedisTemplate redisTemplate) { + REDIS_HOLDER.put(beanName, redisTemplate); + } + + /** + * 从静态变量ApplicationContext中取得Bean, 自动转型为所赋值对象的类型. + */ + @SuppressWarnings("unchecked") + public static T getBean(String name) { + checkApplicationContext(); + if (applicationContext.containsBean(name)) { + return (T) applicationContext.getBean(name); + } + return null; + } + + private static void checkApplicationContext() { + if (applicationContext == null) { + throw new IllegalStateException("applicaitonContext未注入,请检查配置"); + } + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + RedisInstanceFactory.applicationContext = applicationContext; + } + + /** + * 线程池工程 + */ + private static class PoolBuilderFactory { + public GenericObjectPoolConfig getPoolConfig(MagicRedisProperties.Pool properties) { + GenericObjectPoolConfig config = new GenericObjectPoolConfig(); + config.setMaxTotal(properties.getMaxActive()); + config.setMaxIdle(properties.getMaxIdle()); + config.setMinIdle(properties.getMinIdle()); + if (properties.getMaxWait() != null) { + config.setMaxWaitMillis(properties.getMaxWait().toMillis()); + } + return config; + } + } +} \ No newline at end of file diff --git a/magic-api-plugins/magic-api-plugin-dynamic-redis/src/main/java/org/ssssssss/magicapi/redis/RedisResource.java b/magic-api-plugins/magic-api-plugin-dynamic-redis/src/main/java/org/ssssssss/magicapi/redis/RedisResource.java new file mode 100644 index 00000000..40de01ad --- /dev/null +++ b/magic-api-plugins/magic-api-plugin-dynamic-redis/src/main/java/org/ssssssss/magicapi/redis/RedisResource.java @@ -0,0 +1,122 @@ +package org.ssssssss.magicapi.redis; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.data.redis.core.Cursor; +import org.springframework.data.redis.core.RedisCallback; +import org.springframework.data.redis.core.ScanOptions; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.ssssssss.magicapi.core.resource.KeyValueResource; +import org.ssssssss.magicapi.core.resource.Resource; + +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; + +/** + * redis 多数据源插件模块 + * @since 3.0.0 + * @author 冰点 + */ +public class RedisResource extends KeyValueResource { + + private static final Logger logger = LoggerFactory.getLogger(RedisResource.class); + private final StringRedisTemplate redisTemplate; + private final Map cachedContent = new ConcurrentHashMap<>(); + + public RedisResource(StringRedisTemplate redisTemplate, String path, boolean readonly, RedisResource parent) { + super(":", path, readonly, parent); + this.redisTemplate = redisTemplate; + } + + public RedisResource(StringRedisTemplate redisTemplate, String path, boolean readonly) { + this(redisTemplate, path, readonly, null); + } + + @Override + public void readAll() { + List keys = new ArrayList<>(keys()); + List values = redisTemplate.opsForValue().multiGet(keys); + this.cachedContent.entrySet().removeIf(entry -> entry.getKey().startsWith(path)); + if (values != null) { + for (int i = 0, size = keys.size(); i < size; i++) { + this.cachedContent.put(keys.get(i), values.get(i)); + } + } + } + + @Override + public byte[] read() { + String value = this.cachedContent.get(path); + if (value == null) { + value = redisTemplate.opsForValue().get(path); + if (value != null) { + this.cachedContent.put(path, value); + } + } + return value == null ? new byte[0] : value.getBytes(StandardCharsets.UTF_8); + } + + @Override + public boolean write(String content) { + this.redisTemplate.opsForValue().set(this.path, content); + this.cachedContent.put(this.path, content); + return true; + } + + @Override + protected boolean renameTo(Map renameKeys) { + renameKeys.forEach(this.redisTemplate::rename); + renameKeys.forEach((oldKey, newKey) -> this.cachedContent.put(newKey, this.cachedContent.remove(oldKey))); + return true; + } + + @Override + public boolean exists() { + if (this.cachedContent.get(this.path) != null) { + return true; + } + return Boolean.TRUE.equals(this.redisTemplate.hasKey(this.path)); + } + + + @Override + protected boolean deleteByKey(String key) { + if (Boolean.TRUE.equals(this.redisTemplate.delete(key))) { + this.cachedContent.remove(key); + return true; + } + return false; + } + + @Override + protected Function mappedFunction() { + return (it) -> new RedisResource(this.redisTemplate, it, readonly, this); + } + + @Override + protected Set keys() { + Set keys = this.redisTemplate.execute((RedisCallback>) connection -> { + ScanOptions options = ScanOptions.scanOptions() + .count(Long.MAX_VALUE) + .match((isDirectory() ? this.path : (this.path + separator)) + "*") + .build(); + Set returnKeys = new HashSet<>(); + try (Cursor cursor = connection.scan(options)) { + while (cursor.hasNext()) { + returnKeys.add(new String(cursor.next())); + } + } catch (Exception e) { + logger.error("扫描key出错", e); + } + return returnKeys; + }); + return keys == null ? Collections.emptySet() : keys; + } + + @Override + public String toString() { + return String.format("redis://%s", getAbsolutePath()); + } +} diff --git a/magic-api-plugins/magic-api-plugin-dynamic-redis/src/main/resources/META-INF/spring.factories b/magic-api-plugins/magic-api-plugin-dynamic-redis/src/main/resources/META-INF/spring.factories new file mode 100644 index 00000000..1031acc3 --- /dev/null +++ b/magic-api-plugins/magic-api-plugin-dynamic-redis/src/main/resources/META-INF/spring.factories @@ -0,0 +1 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=org.ssssssss.magicapi.redis.MagicDynamicRedisAutoConfig diff --git a/magic-api-plugins/magic-api-plugin-dynamic-redis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/magic-api-plugins/magic-api-plugin-dynamic-redis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 00000000..42df19d1 --- /dev/null +++ b/magic-api-plugins/magic-api-plugin-dynamic-redis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +org.ssssssss.magicapi.redis.MagicRedisConfiguration \ No newline at end of file