likes
comments
collection
share

跟我一起学开源设计第2节:SpringBoot Starter组件中的4项高级设计

作者站长头像
站长
· 阅读数 1

一、背景

我分享了基于一个开源的用户行为埋点的SDK组件封装了一个简易的Spring Boot Starter组件,同时分享了开发一个Spring Boot Starter组件的基本必备的套路: 

(1)、第1步:定义一个XXXProperties的类文件,用于抽象化原有的配置属性与增加新的属性。

(2)、第2步:将核心的业务处理类,初始化核心业务处理类并注入到IOC中,通常写在XXXAutoConfiguration的类文件文件中。

(3)、第3步:为了防止使用者与Starter中包名路径不一致,声明一个spring.factories的文件,来提供一种扫描类到IOC中的途径。

 本节继续分享一点内容,本节分享一些Spring Boot Starter组件中值得学习的高级配置。

本文的内容首发于个人的知识星球:觉醒的新世界程序员。

二、Spring Boot Starter中的常见注解

对于一个Starter组件来说,通常完成了基础的功能后,为了保证不同用户对于客户端使用的需求,通常会开发一些高级配置来满足一些情况的兼容处理,然后这些配置再使用的时候,也会用到大量的注解来提供灵活的配置。其中,这些高级配置可以让我们实现如下玩法:

(1)、基于Enable模式、ConditionOnProperty注解,实现对Spring Boot Starter的开关控制

(2)、基于SPI声明文件,实现不同包环境下的自动装配类加载

(3)、基于AOP注解,实现对某些接口或者配置的自动拦截、代码增强等

(4)、基于Customizer模式,来优雅的扩展组件的集成

。。。。。。

在这个过程中,大概会使用到如下的注解:

@Profile注解:用于区分环境来加载不同的自动装配类

@EnableConfigurationProperties注解:用于装配导入一个属性配置文件,通常结合@ConfigurationProperties来使用

@ConditionalOnClass注解:用于标识当前类路径中存在某个类的时候,才触发自动装配类

@ConditionalOnMissingClass注解:用于标识当前类路径中不存在某个类的时候,才触发

@ConditionalOnMissingBean注解:用于标识当前IOC容器中不存在某个Bean的时候,才触发

@AutoConfigureAfter、@AutoConfigureBefore注解:用于控制先后顺序的注解

其实还有很多注解会在我们开发一个Starter中进行使用,感兴趣的小伙伴可以去继续整理哦。

三、Spring Boot Starter的学习方法

这里先分享一个方法,然后再开始本文的文章。对于这种的学习,在学习到一个基础的组件设计和封装以后,就可以采用题海战术来学习了,同时个人也认为这种方法也是一种通过大量的阅读和学习输入,来持续的让自己先达到量变,在达到质变的一个过程,也会让自己持续沉淀和总结的一个方法,同时也是左耳朵耗子叔以前经常分享的知识学习中的第一步骤:知识采集(感兴趣的可以阅读下极客时间的左耳听风专栏中的知识学习的几篇文章)。

那么如何做呢?不知道大家是否看过肖央老师主演的误杀中的一个场景,就是它看了1000部+的电影,学会了蒙太奇手法来解决问题,也让自己具备了一定的处理问题的技能与思维,那么针对我们的这种需求,也是如此,如果我们给自己灌输了5-10+的Starter组件的阅读与学习这种输入,我相信通过自己的总结和观察,是可以在一定程度上具备了质变,对于Starter的设计思路和代码扩展也是非常有帮助的。

具体怎么干呢?很简单,登录到Github中,搜索【spring-boot-starter】关键词,在出现的结果中,挑选自己感兴趣的5-10+的Starter组件,挨个阅读、分析、总结,并思考每项内容,解决的问题是什么。

如下所示:

跟我一起学开源设计第2节:SpringBoot Starter组件中的4项高级设计

四、Spring Boot Starter组件封装中的几项高级设计

4.1、基于Customizer模式来给Starter组件提供扩展口

Customizer模式这种设计,可以让我们在Starter中埋一个扩展点,这样引用了starter组件的客户端项目可以在Starter组件的某些阶段,对相关的属性和对象进行操作。这里以mybatis的spring-boot-starter组件为例。

仓库地址:github.com/mybatis/spr…

首先,我们定义一个函数式接口的Customizer模式的类:

@FunctionalInterface
public interface ConfigurationCustomizer {
    
    /**
     * Customize the given a {@link Configuration} object.
     * @param configuration the configuratio object to customize
     */
    void customize(Configuration configuration);
}

然后再埋点的地方,去从容器中加载该类的子类,并做循环初始化调用操作:

private void applyConfiguration(SqlSessionFactoryBean factory) {
        MybatisProperties.CoreConfiguration coreConfiguration = this.properties.getConfiguration();
        Configuration configuration = null;
        if (coreConfiguration != null || !StringUtils.hasText(this.properties.getConfigLocation())) {
        configuration = new Configuration();
        }
        if (configuration != null && coreConfiguration != null) {
        coreConfiguration.applyTo(configuration);
        }
        if (configuration != null && !CollectionUtils.isEmpty(this.configurationCustomizers)) {
        // 在这里进行扩展点的循环调用,让子类和扩展类进行自定义处理
        for (ConfigurationCustomizer customizer : this.configurationCustomizers) {
        customizer.customize(configuration);
        }
        }
        factory.setConfiguration(configuration);
}

4.2、基于Enable模式来装配是否启用之类的操作

这里以rocketmq的spring-boot-starter为例,仓库地址如下: github.com/rocketmq/ro… 首先我们在自定义好一个autoconfiguration类后,如果不想让客户端自动装配上,可以提供一个Enable命名为开头的类,来通过这种方式启用装配,代码样例如下:

/**
 * Annotation to enable a RocketMQ implementation.
 *
 * @author He Jialin
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(RocketMQAutoConfiguration.class)
public @interface EnableRocketMQ {
}

我们定义好这样的一个注解,使用时在启动类上面进行声明即可。然后再Auto装配类中,我们还可以结合ConditionalOnProperty注解来表达,某个属性等于某个值的时候,才触发某些装在,代码如下所示:

  @Bean
    @ConditionalOnClass(DefaultMQProducer.class)
    @ConditionalOnMissingBean(DefaultMQProducer.class)
    @ConditionalOnProperty(prefix = "spring.rocketmq", value = {"nameServer", "producer.group"})
    public DefaultMQProducer mqProducer(RocketMQProperties rocketMQProperties) {
        //省略部分代码
      return producer;
    }

4.3、基于SPI声明文件,实现不同包环境下的自动装配类加载

SpringBoot Starter组件在设计时,我们可以看到很多设计者都提供了spring.factories文件,那么这个文件有必要在每个组件中都设置吗,答案不一定。没有这个文件,Starter也能运行,只是需要自己处理和考虑下扫描范围的wet。

这个文件解决的是什么,是那种当前客户端的项目的包名与该文件中的包名不一致的时候,比较有作用。这样当前客户端项目的工程,也可以扫描并加载到其他包下面的加载类。

再Spring Boot 2.7后,这个文件过时了,后续版本会取消,那么新版本的约定是怎么样的规则呢,这里以wxjava的spring-boot-starter组件为例,仓库地址如下:

github.com/Wechat-Grou…

在该工程示例中,在resources文件下定义了如下文件:

META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

在该文件中直接定义了实现类,接口声明在体现在了文件名字上。

com.binarywang.spring.starter.wxjava.miniapp.config.WxMaAutoConfiguration

我们问问ChatGPT怎么看待2.7版本后的这个改动,问问ChatGPT:

Spring Boot 2.7 更改了SPI文件的规则,去掉了spring.factories文件,请帮忙解释下这个东西,以及说说他为什么要更改SPI文件的规则变成另外一种方式,这么做的好处是什么

ChatGPT回答如下:

Spring Boot 2.7 中对 SPI(Service Provider Interface)文件的规则进行了更改,尤其是与以前版本中的 spring.factories 文件相关的部分。在早期版本的 Spring Boot 中,spring.factories 文件被广泛用于配置自动配置类(auto-configuration classes),以及其他类型的服务和组件。

Spring Boot 2.7 中的更改

规则更改: Spring Boot 2.7 取消了 spring.factories 文件的使用,转而采用新的方式来管理 SPI。

新的方式: 这可能包括使用基于注解的配置,或者采用新的配置文件格式来注册和发现服务。

更改的原因

简化配置: 新的方法可能旨在简化配置过程,使得自动配置和服务的管理更加直观和易于理解。

性能优化: 改变 SPI 文件的规则可能是为了提高应用启动和运行时的性能。

增加灵活性: 新的机制可能提供了更大的灵活性,允许更精细的控制和定制。

好处

提高可读性和易维护性: 通过更直观的配置方法,使得代码更易于阅读和维护。

性能提升: 可能改善了启动时间和运行效率。

更好的自定义支持: 新机制可能更好地支持自定义和扩展。

4.4、基于AOP注解,实现对某些接口或者配置的自动拦截、代码增强等

在开发Starter组件的过程中,有些Starter组件的目标是对系统功能进行拦截、自动增强等处理,此时,我们又可以用一个AOP思维,来定义个增强类的Starter组件,这里以一个Redis的ratelimiter-spring-boot-starter的限流的Starter组件为例,仓库地址如下:

github.com/taptap/rate…

首先,可以定义一个自定义注解:

@Target(value = {ElementType.METHOD})
@Retention(value = RetentionPolicy.RUNTIME)
public @interface RateLimit {

    //===================== 公共参数 ============================

    Mode mode() default Mode.TIME_WINDOW;
    /**
     * 时间窗口模式表示每个时间窗口内的请求数量
     * 令牌桶模式表示每秒的令牌生产数量
     * @return rate
     */
    int rate();

    //省略部分代码
}

然后,为这个注解定义一个AOP拦截类:

@Aspect
@Component
@Order(0)
public class RateLimitAspectHandler {

    private static final Logger logger = LoggerFactory.getLogger(RateLimitAspectHandler.class);

    private final RateLimiterService rateLimiterService;
    private final RuleProvider ruleProvider;

    public RateLimitAspectHandler(RateLimiterService lockInfoProvider, RuleProvider ruleProvider) {
        this.rateLimiterService = lockInfoProvider;
        this.ruleProvider = ruleProvider;
    }

    @Around(value = "@annotation(rateLimit)")
    public Object around(ProceedingJoinPoint joinPoint, RateLimit rateLimit) throws Throwable {
        Rule rule = ruleProvider.getRateLimiterRule(joinPoint, rateLimit);

        Result result = rateLimiterService.isAllowed(rule);
        boolean allowed = result.isAllow();
        if (!allowed) {
            logger.info("Trigger current limiting,key:{}", rule.getKey());
            if (StringUtils.hasLength(rule.getFallbackFunction())) {
                return ruleProvider.executeFunction(rule.getFallbackFunction(), joinPoint);
            }
            long extra = result.getExtra();
            throw new RateLimitException("Too Many Requests", extra, rule.getMode());
        }
        return joinPoint.proceed();
    }


}

这段的核心配置其实是@Around(value = "@annotation(rateLimit)")这个代码,通过这个环绕通知的切面拦截,可以实现一种AOP的Starter的自动增强处理。

然后再AutoConfiguration类中,去导入这个AOP类:

@Configuration
@ConditionalOnProperty(prefix = RateLimiterProperties.PREFIX, name = "enabled", havingValue = "true")
@AutoConfigureAfter(RedisAutoConfiguration.class)
@EnableConfigurationProperties(RateLimiterProperties.class)
@Import({RateLimitAspectHandler.class, RateLimitExceptionHandler.class})
public class RateLimiterAutoConfiguration {

    private final RateLimiterProperties limiterProperties;
    public final static String REDISSON_BEAN_NAME = "rateLimiterRedissonBeanName";

    public RateLimiterAutoConfiguration(RateLimiterProperties limiterProperties) {
        this.limiterProperties = limiterProperties;
    }
    // 省略部分代码
}

同时结合我们刚开始说过的一些注解,实现比较优雅的设计。

五、总结

本文总结了4种Spring Boot Starter的高级设计,通过阅读一些开源的设计,可以有效的提高我们对于Starter组件封装的技能与认识:

跟我一起学开源设计第2节:SpringBoot Starter组件中的4项高级设计

本文也是近期想做的内容的第二篇总结,后续的内容就是分享Growing IO SDK的源码分析与设计,希望可以让大家学习到一种客户端数据埋点与上报的代码设计。

在此过程中是否有困惑和疑问想要交流的呢,欢迎一起交流。

六、练习

感兴趣的小伙伴,既然学习了,也不能白学习,可以尝试去Github上,搜索自己感兴趣的1-5个Starter组件,在了解技术的同时,可以进行思考,并进行分析,同时建议用Start法则来描述自己做的事情。

什么是Start法则呢,看下以下描述:

“S”—— situation,背景或环境。

“T”—— task,制定任务。

“A”—— action,实施步骤。

“R”—— result,结果反响。

“T”—— think,思考改进。

比如针对这个问题/任务,当时遇到了一个怎么样的背景问题,在分析问题的过程中,给自己制定了怎样的任务目标或者期望,然后采取了怎样的实施步骤,得到了什么结果,效果如何,最后针对已知的结果,思考未来可以怎样改进,来得到更好的效果。

同时本文也是作者首发于个人知识星球【觉醒的新世界程序员】中的一篇文章,也是在星球内正在分享的《开源设计系列专题》中的中的内容,如果想了解相关内容,可以来了解下,与我一同学习交流。

非常期待与大家一起学习、进步,喜欢的老铁可以收藏、点赞、关注、分享哦。

欢迎在评论区一起交流哦。