昨天思考了这样一个问题, 对于多服务系统来说, 存取缓存的特性服务肯定是大多数服务都需要的, 这样的话, 那岂不是每个服务都要实现操作缓存的代码, 工具类, 配置类?
似乎思考下来有几种实现方式:
spring-boot-starter-data-redis
再封装一层, 加入我们自己的工具类 (该工具类实现了一些自定义的设定), 屏蔽相关配置, 做成 Starter 是否可行? 这是昨天下班前的想法, 现在就尝试实现.本文介绍如何把 spring-boot-starter-data-redis 二次封装成 Starter, 这个 Starter 毫无疑问会包含我们自己的配置, Redis 工具类等…目的就是让其他服务不用各自再实现自己的逻辑, 只需引入这个自定义的 Starter 即可.
在 第一篇关于 Starter 的文章 中简单说明了下 Starter 是什么, 本文就不再赘述, 只提及第一篇中没提及的概念和内容.
第一步, 创建一个简单的 Maven 项目, 命名1 为 data-redis-service-spring-boot-starter, 配置并引入相关依赖.
<?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>
<groupId>cn.caplike</groupId>
<artifactId>data-redis-service-spring-boot-starter</artifactId>
<version>1.0.0-SNAPSHOT</version>
<!-- 节省篇幅, 省略部分常规设定 -->
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.68</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.10</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.12</version>
<scope>provided</scope>
</dependency>
</dependencies>
</project>
新建一个类, 目的是为了 启用 spring-boot-starter-data-redis
的自动配置.
/**
* 启用 spring-boot-starter-data-redis 的自动配置
*
* @author LiKe
* @version 1.0.0
* @date 2020-05-10 16:22
*/
@EnableAutoConfiguration
public class RedisAutoConfigure{
}
自动配置类, 这个是重点, 这里我自定义了简单的配置, 用 fastJson 作为序列化和反序列化工具. 代码如下:
/**
* 自动配置类
*
* @author LiKe
* @version 1.0.0
* @date 2020-05-10 13:42
*/
@Configuration
@ConditionalOnClass(RedisService.class)
public class RedisServiceAutoConfigure {
private RedisTemplate<String, Object> redisTemplate;
@Bean
@ConditionalOnMissingBean
public RedisService redisService() {
return new RedisService(redisTemplate);
}
@Autowired
public void setRedisTemplate(RedisTemplate<String, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
}
// ~ redis configuration
// -----------------------------------------------------------------------------------------------------------------
/**
* key 的序列化器
*/
private final StringRedisSerializer keyRedisSerializer = new StringRedisSerializer();
/**
* value 的序列化器
*/
private final RedisFastJsonSerializer<Object> valueRedisSerializer = new RedisFastJsonSerializer<>(Object.class);
@Bean
public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
// RedisCacheWriter
RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory);
// RedisCacheConfiguration - 值的序列化方式
RedisSerializationContext.SerializationPair<Object> serializationPair = RedisSerializationContext.SerializationPair.fromSerializer(valueRedisSerializer);
RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig().serializeValuesWith(serializationPair);
return new RedisCacheManager(redisCacheWriter, redisCacheConfiguration);
}
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
// 配置连接工厂
redisTemplate.setConnectionFactory(redisConnectionFactory);
// 值序列化-RedisFastJsonSerializer
redisTemplate.setValueSerializer(valueRedisSerializer);
redisTemplate.setHashValueSerializer(valueRedisSerializer);
// 键序列化-StringRedisSerializer
redisTemplate.setKeySerializer(keyRedisSerializer);
redisTemplate.setHashKeySerializer(keyRedisSerializer);
return redisTemplate;
}
}
这个类还是很简单的, 满足指定条件下, 配置出自定义的 RedisService. @ConditionalOnXxx 注解简单介绍一下, 代码中 @ConditionalOnClass(RedisService.class)
当 classpath 下发现 RedisService 时才进行自动配置; @ConditionalOnMissingBean
, 当上下文中不存在该 Bean 时才创建;
其中, RedisFastJsonSerializer 是自定义的序列化和反序列化工具, 为了往 Redis 中设值的时候, 自动将对象序列化, 取值的时候自动反序列化, 代码如下:
/**
* RedisFastJsonSerializer 是自定义的序列化和反序列化工具,
* 为了往 Redis 中设值的时候, 自动将对象序列化, 取值的时候自动反序列化
*
* @author LiKe
* @version 1.0.0
* @date 2020-05-10 18:58
*/
public class RedisFastJsonSerializer<T> implements RedisSerializer<T> {
private final Class<T> clazz;
public RedisFastJsonSerializer(Class<T> clazz) {
this.clazz = clazz;
}
@Override
public byte[] serialize(T t) throws SerializationException {
if (Objects.isNull(t)) {
return new byte[0];
}
return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(StandardCharsets.UTF_8);
}
@Override
public T deserialize(byte[] bytes) throws SerializationException {
if (Objects.isNull(bytes) || ArrayUtils.isEmpty(bytes)) {
return null;
}
return JSON.parseObject(new String(bytes, StandardCharsets.UTF_8), clazz);
}
}
※ 下面列举 SpringBoot 中的所有 Conditional 注解及作用:
注解名 | 描述 |
---|---|
@ConditionalOnBean | 当容器中有指定的Bean的条件下 |
@ConditionalOnClass | 当类路径下有指定的类的条件下 |
@ConditionalOnExpression | 基于SpEL表达式作为判断条件 |
@ConditionalOnJava | 基于JVM版本作为判断条件 |
@ConditionalOnJndi | 在JNDI存在的条件下查找指定的位置 |
@ConditionalOnMissingBean | 当容器中没有指定Bean的情况下 |
@ConditionalOnMissingClass | 当类路径下没有指定的类的条件下 |
@ConditionalOnNotWebApplication | 当前项目不是Web项目的条件下 |
@ConditionalOnWebApplication | 当前项目是Web项目的条件下 |
@ConditionalOnProperty | 指定的属性是否有指定的值 |
@ConditionalOnResource | 类路径下是否有指定的资源 |
@ConditionalOnSingleCandidate | 当指定的Bean在容器中只有一个, 或者在有多个Bean的情况下, 用来指定首选的Bean |
这个类就是要暴露给其他服务调用的核心类, 封装了对 Redis 的相关操作, 用前面构建的 RedisTemplate 来构造, 核心代码主要是构造函数:
public final class RedisService {
private final RedisTemplate<String, Object> redisTemplate;
// ~ value set & get
// -----------------------------------------------------------------------------------------------------------------
public RedisService(RedisTemplate<String, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
}
// ~ 节省篇幅, 省略部分对本文来说不是很重要的代码
第一步, 在 resources 下新建 profiles 为 data-redis-service 的配置文件: application-data-redis-service.yml (目的是为了避免配置文件冲突, 且让 Starter 中的配置可被覆盖, 稍后会介绍到), 内容是 Redis 的配置,
spring:
redis:
host: >
port: >
password: >
database: 0
第二步, 在 resources/META-INF/ 下创建 spring.factories 文件, 并添加如下内容:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
cn.caplike.data.redis.service.spring.boot.starter.RedisServiceAutoConfigure
根目录运行 mvn clean install
打包到仓库.
新建一个测试用的 SpringBoot 项目, 引入我们的 Starter:
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>cn.caplikegroupId>
<artifactId>data-redis-service-spring-boot-starterartifactId>
<version>1.0.0-SNAPSHOTversion>
dependency>
dependencies>
可以看到 data-redis-service-spring-boot-starter 的相关依赖也同样被传递到上下文中了, 我们甚至都不需要再添加 spring-boot-starter-data-redis
的依赖.
在测试项目的配置文件中启用 Starter 中的配置 (如果我们不想应用默认的配置, 也可以直接在这里写 Redis 的配置, 覆盖 Starter 中的配置):
spring:
profiles:
active: data-redis-service
server:
port: 13706
新建一个测试用 Controller, 注入 Starter 中已经封装好的 RedisService:
private RedisService redisService;
@PostMapping("/set")
public void set() {
final RedisService.Key key =
RedisService.Key.builder().prefix("author").suffix("name").build();
redisService.setValue( key,
new User("黄金甲壳虫", "A pretty strong credential."), 60);
final User user = redisService.getValue(key, User.class);
System.out.println(
"user: " + user.getUsername() + " / " + user.getPassword()
);
}
@Autowired
public void setRedisService(RedisService redisService) {
this.redisService = redisService;
}
启动工程用 Postman 测试
可以看到已经注值成功:
反序列化也正常:
这种方式的封装比较灵活, 又不用单独起一个服务. 还可以根据实际情况复写 Starter 中的默认配置, 也保证了灵活性.
以同样的思路可以运用到诸如日志等的多种已有 Starter, 在其上再做一层我们自己的封装, 新增个性化的设定.
SpringBoot 的 Starter 还有很多细节没有研究透彻…以后继续.
~ END ~
官方 Starter 以 spring-boot-starter-xxx 的方式命名. 同时也是官方建议自定义的 Starter 用 xxx-spring-boot-starter 命名. 以作区分. ↩︎