SpringBoot 自动配置 (2) - 自己写个 Starter 二次封装 spring-boot-starter-data-redis

前言

昨天思考了这样一个问题, 对于多服务系统来说, 存取缓存的特性服务肯定是大多数服务都需要的, 这样的话, 那岂不是每个服务都要实现操作缓存的代码, 工具类, 配置类?
似乎思考下来有几种实现方式:

  1. 确确实实把缓存单独做成一个服务, 关于缓存的配置, 序列化与反序列化设置, 工具类都在这个服务中, 单独跑起来. 这样似乎确确实实能够起到"公共抽取"的目的, 但是其他服务对缓存服务的调用仍然有代码量, 编写依托 Feign 的接口调用类也仍然每个服务中都需要一份 COPY;
  2. 要不做成 JAR 包? 认为不太可行, 因为我们本身期望利用 SpringBoot 的 AutoConfigure 简化 Redis 的配置, 相当于基于 Redis SpringBoot 的 AutoConfigure 再封装. 做成 JAR 无法利用这一点, 更像是从头到尾自己实现对接 Redis;
  3. 利用 SpringBoot 的 AutoConfigure 把 Redis 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, 配置并引入相关依赖.

pom.xml

<?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>

RedisAutoConfigure

新建一个类, 目的是为了 启用 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{
}

RedisServiceAutoConfigure

自动配置类, 这个是重点, 这里我自定义了简单的配置, 用 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

RedisService

这个类就是要暴露给其他服务调用的核心类, 封装了对 Redis 的相关操作, 用前面构建的 RedisTemplate 来构造, 核心代码主要是构造函数:

public final class RedisService {

    private final RedisTemplate<String, Object> redisTemplate;

    // ~ value set & get
    // -----------------------------------------------------------------------------------------------------------------

    public RedisService(RedisTemplate<String, Object> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

	// ~ 节省篇幅, 省略部分对本文来说不是很重要的代码

配置 Starter

第一步, 在 resources 下新建 profiles 为 data-redis-service 的配置文件: application-data-redis-service.yml (目的是为了避免配置文件冲突, 且让 Starter 中的配置可被覆盖, 稍后会介绍到), 内容是 Redis 的配置,

spring:
  redis:
    host: <host>
    port: <port>
    password: <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.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <dependency>
        <groupId>cn.caplike</groupId>
        <artifactId>data-redis-service-spring-boot-starter</artifactId>
        <version>1.0.0-SNAPSHOT</version>
    </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 ~

附录

Gradle 引入

Gradle 引入也非常简单, 只需要做如下配置 (指定仓库地址, 引入 data-redis-spring-boot-starter):

plugins {
    id 'java'
}

group 'cn.caplike'
version '1.0.0-SNAPSHOT'

repositories {
    maven { url 'file://D:/caplike/coding/tools/build/apache-maven-3.6.3/repository' }
}

dependencies {
    compile group: 'org.springframework.boot', name: 'spring-boot-starter-data-redis', version: '2.2.1.RELEASE'
    compile group: 'cn.caplike', name: 'data-redis-service-spring-boot-starter', version: '1.0.0-SNAPSHOT'

    testCompile group: 'junit', name: 'junit', version: '4.12'
}

在这里插入图片描述


  1. 官方 Starter 以 spring-boot-starter-xxx 的方式命名. 同时也是官方建议自定义的 Starter 用 xxx-spring-boot-starter 命名. 以作区分. ↩︎

评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值