跟我一起学开源设计第1节:封装埋点GrowingIO Spring Boot Starter组件

一、背景

Spring Boot已经出现了好多年了,但是可能很多开发者,一直没有合适的机会开发属于自己的Spring Boot Starter组件,今天分享一个真实工作中的例子,分享如何开发一个Spring Boot Starter组件,以及处理这个需求的过程。

二、Spring Boot Starter介绍

首先来看一段AI大模型对它的介绍:

Spring Boot Starter 是Spring Boot生态系统中的重要组成部分,它为开发者提供了一种快速启动Spring Boot应用程序的方式。

首先,Spring Boot Starter是什么?

Spring Boot Starter可以被理解为一种依赖的集合,也可以看作是一个空的项目,它由pom.xml文件配置了一堆jar包的组合。这些jar包都是预先配置好的,并且包含了各种不同的功能模块。例如,Spring

Boot Starter Web包含了用于处理HTTP请求的类库,Spring Boot Starter Data JPA包含了用于数据访问的类库等。

其次,Spring Boot Starter解决了什么问题?

Spring Boot Starter解决了手动配置大量依赖项和参数的问题。在Spring Boot之前,如果要开发一个Web应用程序,需要手动添加很多依赖项,如Servlet、JSP、JSTL等,并且还需要配置很多参数,如数据源、事务管理器等。而通过使用Spring Boot Starter,开发者只需要添加一个Starter依赖,就可以轻松地集成各种不同的功能模块,而无需关心底层的配置和集成细节。

最后,Spring Boot Starter的价值是什么?

Spring Boot Starter的价值在于它能够提高开发效率和代码质量,同时减少开发成本和复杂度。通过使用Starter,开发者可以专注于业务逻辑的实现,而不需要关心底层的配置和集成细节。另外,Starter还支持更快的迭代和部署,因为它们通常包含了一些可重用的依赖库和自动配置类。

对于日常的技术与业务学习,能够经常思考这三点是非常重要的。

三、本节需求描述

本节通过针对一个用户行为分析埋点平台的Java SDK进行封装,开发出一个Spring Boot Starter组件,来让大家知道如何开发一个Starter。

虽然我们可能没有注册GrowingIO的用户账号,但是不影响我们开发Starter组件。

先来让AI大模型告诉我们Growing IO是什么软件,解决了什么样的问题?

GrowingIO是一个一站式数据增长引擎整体方案服务商,以数据智能分析为核心,通过构建客户数据平台,打造增长营销闭环,帮助企业提升数据驱动能力,赋能商业决策、实现业务增长。它专注于零售、电商、保险、酒旅航司、教育、内容社区等行业,致力于帮助企业利用数据实现业务增长。

GrowingIO解决了企业在数据驱动业务增长方面的痛点。首先,它提供了一站式的解决方案,帮助企业构建客户数据平台,整合不同渠道的数据,实现数据的统一管理和分析。其次,GrowingIO提供了强大的数据采集和分析功能,可以实时分析用户行为数据,从而更好地了解用户需求和行为,优化产品设计和运营策略。此外,GrowingIO还提供了增长营销闭环的服务,帮助企业制定并执行基于数据的营销策略,提高用户留存率、转化率和变现能力。

通过这个介绍,我们知道了它是一个用户行为分析埋点分析的一个软件,可以在我们业务系统中的一些关键的节点,埋上一个事件操作,上报给服务端的系统,从而进行一些动作与实践的行为分析。

那么就避免不了要开发客户端的SDK,去上报一些动作的事件信息,这里我们选择了Java SDK。

我们先来看没有使用Starter的时候,如何使用这个Java SDK,这里不具体描述,大家可以看下如下链接中的介绍:

官方SDK说明文档:

docs.growingio.com/v3/develope...

官方SDK源码地址:

github.com/growingio/g...

建议大家花1-5分钟过一下上面的例子,明白它做的事情。

为什么,要做这样一个Starter呢。

我的原因是:

(1)、目前它家暂未提供公开的Starter组件,用它们这个原始的组件,集成起来不方便,同时需要自定义properties文件,无法在微服务架构下比较容易的与配置中心集成。

(2)、目前它的growingio sdk存在版本不同的区别,后续sdk版本升级,和业务代码耦合,可能仍需改动,所以需要进行二次封装,采用灵活的starter的方式。

(3)、屏蔽原生的java SDK API实现,同时提供简单且友好的API方便调用,从一定程度上对业务代码进行防腐与主动权控制。

(4)、GrowingIO默认的Java埋点SDK默认的API调用方法,封装较少,业务开发时可能需要进行某些转换与统一处理等。

以上这几点,也算是我们本次的一个需求,基于以上几点我们进行封装,同时这几点也是带着问题去思考的描述,开发完成后,设计上是怎么解决这几点的。日常面试中,如果自己主动的想表达自己具备封装Starter的经验,那么可能面试官也会问你为什么要做这样一个事情。

四、Spring Boot Starter封装的套路

任何事情做起来都是有套路的,Spring Boot Starter也不例外,掌握了某个事情的套路,会让自己具备一种结构化的表达和内容的分享,对于类似的东西,其实只需要了解套路是什么,第1步骤,第2步骤是什么。。。。这样内在的本质原因不会变,剩下的只是时间上的问题。

这里分享一下简单的Starter封装的套路,本文暂时只介绍一个最小配置的Starter的开发方式。

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

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

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

以上是核心的3点,具备这基础的3点,可以完成一个基础的starter小组件。

在高级一点,我们可以继续增加如下内容:

(4)、定义一个XXXXEnable模式+@Import模式的注解,用于控制Starter是否生效与动态注册对象Bean到IOC容器中。

(5)、使用很多注解,来区分当前starter组件中的先后顺序、环境区分、兼容性等等。

(6)、自定义一个AOP类和一个注解,用于动态标识哪些方法进行业务埋点操作,避免一定程度上的代码侵入。

总结如下:

五、GrowingIO Spring Boot Starter封装开始

5.1、第一步:新建项目工程,并抽象化SDK的gio.properties文件内容

1、新建工程,工程名称这里定义为:growingio-spring-boot-stater

2、导入GrowingIO SDK等相关依赖

xml 复制代码
 <dependencies>
        <dependency>
            <groupId>io.growing.sdk.java</groupId>
            <artifactId>growingio-java-sdk</artifactId>
            <version>1.0.8</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <version>2.4.13</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <version>2.4.13</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.28</version>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.12.0</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>1.7.26</version>
        </dependency>
    </dependencies>

3、创建项目结构与抽象化gio.properties文件(该文件的样例在上面分享那个Github链接中),并创建相关的常量类,如下所示,核心的Properties代码如下:

ini 复制代码
@ConfigurationProperties(GrowingioProperties.GROWINGIO_PREFIX)
@Data
public class GrowingioProperties {
    
    /**
     * 默认前缀
     */
    public static final String GROWINGIO_PREFIX = "growingio";
    
    /**
     * 项目采集端地址
     */
    private String apiHost;
    
    /**
     * 项目ID
     */
    private String projectId;
    
    /**
     * 消息发送间隔时间,单位ms(默认 100)
     */
    private Integer sendMsgInterval = 100;
    
    /**
     * 消息发送线程数量,默认为3
     */
    private Integer sendMsgThread = 3;
    
    /**
     * 消息队列大小
     */
    private Integer msgStoreQueueSize = 500;
    
    /**
     * 数据压缩 false:不压缩, true:压缩 不压缩可节省cpu,压缩可省带宽
     */
    private Boolean compress = true;
    
    /**
     * 日志输出级别(debug | error)
     */
    private String loggerLevel = "debug";
    
    /**
     * 自定义日志输出实现类
     */
    private String loggerImplemention = "cn.aijavapro.growingio.log.GrowingioLogger";
    
    /**
     * 运行模式,test:仅输出消息体,不发送消息,production:发送消息
     */
    private String runMode = "test";
    
    /**
     * http 连接超时时间,默认2000ms
     */
    private Integer connectionTimeout = 2000;
    
    /**
     * http 连接读取时间,默认2000ms
     */
    private Integer readTimeout = 2000;
    
    /**
     * 是否启用:自定义属性:标识是否启用,默认为不启用,非growing io 官方属性
     */
    private Boolean enable = false;
   
}

对于一个配置属性类来说,通常会使用ConfigurationProperties注解来定义属性配置。这个也是套路

然后GrowingioConstant的代码如下,只是封装了SDK的属性文件中的Key:

arduino 复制代码
public class GrowingioConstant {
    
    /**
     * 项目采集端地址
     */
    public static final String API_HOST_KEY = "api.host";
    
    /**
     * 项目ID
     */
    public static final String PROJECT_ID_KEY = "project.id";
    
    /**
     * 消息发送间隔时间,单位ms(默认 100)
     */
    public static final String SEND_MSG_INTERVAL_KEY = "send.msg.interval";
    
    /**
     * 消息发送线程数量,默认为3
     */
    public static final String SEND_MSG_THREAD_KEY = "send.msg.thread";
    
    /**
     * 消息队列大小
     */
    public static final String MSG_STORE_QUEUE_SIZE_KEY = "msg.store.queue.size";
    
    /**
     * 数据压缩 false:不压缩, true:压缩 不压缩可节省cpu,压缩可省带宽
     */
    public static final String COMPRESS_KEY = "compress";
    
    /**
     * 日志输出级别(debug | error)
     */
    public static final String LOGGER_LEVEL_KEY = "logger.level";
    
    /**
     * 自定义日志输出实现类
     */
    public static final String LOGGER_IMPL_KEY = "logger.implemention";
    
    /**
     * 运行模式,test:仅输出消息体,不发送消息,production:发送消息
     */
    public static final String RUN_MODE_KEY = "run.mode";
    
    /**
     * http 连接超时时间,默认2000ms
     */
    public static final String CONNECTION_TIMEOUT_KEY = "connection.timeout";
    
    /**
     * http 连接读取时间,默认2000ms
     */
    public static final String READ_TIMEOUT_KEY = "read.timeout";
    
    
}

我们自定义一个Growing IO日志的实现类,用于测试阶段的日志输出,不依赖服务端依然可以使用,如下所示:

typescript 复制代码
public class GrowingioLogger implements GioLoggerInterface {
    
    private static final Logger logger = LoggerFactory.getLogger(GrowingioLogger.class);
    
    @Override
    public void debug(String s) {
        logger.info("测试阶段日志输出:{}", s);
    }
    
    @Override
    public void error(String s) {
        logger.error("测试阶段日志输出:{}", s);
    }
}

然后工程结构截图如下所示:

5.2、第二步:定义一个请求参数类,封装一个Service业务类

此步骤的目的也是为使用者提供一个比较清晰易懂的入口业务处理类,避免使用者在业务代码中直接调用原生SDK,因为这个SDK他们家其他版本的与这个有些不一样,避免污染业务代码。

1、首先我们看下原生的Java调用代码:

scss 复制代码
//事件行为消息体
GIOEventMessage eventMessage = new GIOEventMessage.Builder()
    .eventTime(System.currentTimeMillis())            // 默认为系统当前时间,选填
    .eventKey("3")                                    // 事件标识 (必填)
    .eventNumValue(1.0)                               // 打点事件数值 (选填)
    .loginUserId("417abcabcabcbac")                   // 带用登陆用户ID的 (选填)
    .addEventVariable("product_name", "苹果")          // 事件级变量 (选填)
    .addEventVariable("product_classify", "水果")      // 事件级变量 (选填)
    .addEventVariable("product_price", 14)            // 事件级变量 (选填)
    .build();

//上传事件行为消息到服务器
GrowingAPI.send(eventMessage);

可以看到这段代码封装了当前一个业务的登录用户信息,和一些事件的变量,但是Growing IO SDK中,未提供其他addEventVariable方法,可能有些不方便,我们可以封装几个核心的参数,通用的参数在Starter层面统一控制,所以我们在封装一层请求参数类,并提供一个Map的具体数据类:

typescript 复制代码
public class EventMessageRequest implements Serializable {
    private static final long serialVersionUID = 892840357641768298L;
    
    private String eventKey;
    
    private String userId;
    
    private Map<String,Object> eventVariableMap;
    
    public EventMessageRequest(String eventKey, String userId, 
            Map<String, Object> eventVariableMap) {
        this.eventKey = eventKey;
        this.userId = userId;
        this.eventVariableMap = eventVariableMap;
    }
}

然后封装一个业务处理实现,同时定义了异常捕获,避免影响到主流程:

scss 复制代码
public class GrowingioServiceImpl implements GrowingioService {
    
    private static final Logger logger = LoggerFactory.getLogger(GrowingioServiceImpl.class);
    
    @Override
    public void sendEventMessage(EventMessageRequest request){
        try{
            if(StringUtils.isEmpty(request.getEventKey()) && StringUtils.isEmpty(request.getUserId())){
                return;
            }
            GIOEventMessage.Builder builder = new GIOEventMessage.Builder()
                    .eventTime(System.currentTimeMillis())
                    .eventKey(request.getEventKey())
                    .loginUserId(request.getUserId());
            Set<Map.Entry<String, Object>> entries = request.getEventVariableMap().entrySet();
            for(Map.Entry<String, Object> entry : entries){
                String key = entry.getKey();
                Object value = entry.getValue();
                if(value != null){
                    if(value instanceof String){
                        builder.addEventVariable(key, ((String) value));
                    }else if(value instanceof Long){
                        builder.addEventVariable(key, ((Long) value).toString());
                    }else if(value instanceof Integer){
                        builder.addEventVariable(key, ((Integer) value));
                    }else if(value instanceof Double){
                        builder.addEventVariable(key, ((Double) value));
                    }else if(value instanceof BigDecimal){
                        builder.addEventVariable(key, ((BigDecimal) value).doubleValue());
                    }else {
                        logger.warn("not support event variable value type:{}", key);
                    }
                }
                
            }
            //事件行为消息体
            GIOEventMessage eventMessage = builder.build();
            //上传事件行为消息到服务器,底层阻塞队列异步处理
            GrowingAPI.send(eventMessage);
        }catch (Exception e){
            logger.error("send growingio event message error:{}" , e.getMessage(),e);
        }
        
    }
    
}

5.3、第三步:自定义一个自动装配类,并且定义一个spi文件

1、首先定义一个autoConfiguration类,代码如下:

scss 复制代码
@Configuration
@EnableConfigurationProperties(GrowingioProperties.class)
public class GrowingioAutoConfiguration{
    
    private static final Logger logger = LoggerFactory.getLogger(GrowingioAutoConfiguration.class);
    
    @Autowired
    protected GrowingioProperties growingioProperties;
    
    @Bean
    public GrowingioService growingioService(){
        return new GrowingioServiceImpl();
    }
    
    public void checkProperties() {
        //校验并开始检查是否配置必填的属性
        if(StringUtils.isEmpty(growingioProperties.getApiHost())){
            throw new RuntimeException("growing properties api.host must be defined");
        }
        if(StringUtils.isEmpty(growingioProperties.getProjectId())){
            throw new RuntimeException("growing properties project.id must be defined");
        }
    }
    
    
    /**
     * 页面初始化执行函数
     */
    @PostConstruct
    private void init(){
        this.checkProperties();
        //初始化配置
        this.initGrowingioApiProperties();
    }
    
    private void initGrowingioApiProperties() {
        Properties properties = new Properties();
        properties.setProperty(GrowingioConstant.API_HOST_KEY, growingioProperties.getApiHost());
        properties.setProperty(GrowingioConstant.PROJECT_ID_KEY, growingioProperties.getProjectId());
        properties.setProperty(GrowingioConstant.SEND_MSG_INTERVAL_KEY, growingioProperties.getSendMsgInterval().toString());
        properties.setProperty(GrowingioConstant.SEND_MSG_THREAD_KEY, growingioProperties.getSendMsgThread().toString());
        properties.setProperty(GrowingioConstant.MSG_STORE_QUEUE_SIZE_KEY, growingioProperties.getMsgStoreQueueSize().toString());
        properties.setProperty(GrowingioConstant.COMPRESS_KEY, growingioProperties.getCompress().toString());
        properties.setProperty(GrowingioConstant.LOGGER_LEVEL_KEY, growingioProperties.getLoggerLevel());
        properties.setProperty(GrowingioConstant.LOGGER_IMPL_KEY, growingioProperties.getLoggerImplemention());
        properties.setProperty(GrowingioConstant.RUN_MODE_KEY, growingioProperties.getRunMode());
        properties.setProperty(GrowingioConstant.CONNECTION_TIMEOUT_KEY, growingioProperties.getConnectionTimeout().toString());
        properties.setProperty(GrowingioConstant.READ_TIMEOUT_KEY, growingioProperties.getReadTimeout().toString());
        //通过SDK中的这个API可以避免使用properties文件
        ConfigUtils.init(properties);
        logger.info("init load growingio starter api properties success,url:{},runmode:{},enable:{}",
                growingioProperties.getApiHost(),growingioProperties.getRunMode(),growingioProperties.getEnable());
    }
    
    
}

然后再resources文件夹下在创建一个meta-inf/spring.factoies文件,为了调用方包名不一样的时候,可以被扫描到:

ini 复制代码
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
    cn.aijavapro.growingio.config.GrowingioAutoConfiguration

这样一个基本的Spring Boot Starter组件就完成了,此时工程截图如下所示:

六、尝试使用

虽然我们没有GrowingIO的账号和服务端的环境,但是我们也是可以测试用的,GrowingIO SDK中提供了一调试测试模式,所以是只要在控制台中能够看到我们的日志输出就代表starter开发成功了。

1、首先,新创建一个Spring Boot工程,然后对刚才的starter项目进行引入,然后定义启动配置参数文件,并启动项目,看看系统启动中是否输出了启动日志。

引入依赖:

xml 复制代码
<dependency>
            <groupId>cn.aijavapro</groupId>
            <artifactId>growingio-spring-boot-stater</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        

创建application.yml文件,并声明核心的几个参数配置,这里指定了runMode为test模式,代表不会真实请求的SDK的服务端。

然后我们在一个Controller中模拟生成订单时候的一个业务埋点操作,代码如下所示:

然后启动项目,看看是否成功输出了启动日志:

然后,我们通过CURL命令或者浏览器直接访问测试请求,看看控制台是否有埋点日志输出

curl http://localhost:8080/order/save

正常会出现如下的结果:

如果看到这个结果,说明一个简易版的Spring Boot Starter已经生效了。如果后续对接服务端,可以直接发送请求到服务端。

还在等什么,赶紧按照我的分享,去动手实践一下吧,实际行动了,才是学习技术最好的老师。

七、总结

今天分享了一个基于现成的SDK,给他封装成Starter的形式,后续我可能会继续分享,比如:

(1)、分享这个SDK的源码设计与思路,可以学到一种Java客户端SDK埋点的开发,异步队列发送等等,对于日常代码设计和工作也有一定的帮助。

(2)、为当前Starter封装几个高级Starter具备的那几个特性。

(3)、尝试模拟开发一个小巧的服务端,用于接收这个客户端SDK上传的埋点日志。

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

八、练习

感兴趣的小伙伴,既然学习了,也不能白学习,可以尝试着做做如下练习练习,往往技术的提高在于一些刻意练习上。比如:

(1)、尝试基于神策分析的Java SDK(神策数据官方 Java 埋点 SDK,是一款轻量级用于 Java 端的数据采集埋点 SDK)封装出一个Spring Boot Starter,神策分析的SDK介绍地址:manual.sensorsdata.cn/sa/latest/z...,在进行这个SDK封装的时候,可以多从使用者的角度思考一下,如果通过封装Starter组件,来降低开发人员的集成难度,提高开发效率。

(2)、尝试基于开源的ChatGPT的Java SDK,封装出一个Spring Boot Starter,SDK的介绍资料如下:github.com/PlexPt/chat...

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

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

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

相关推荐
代码之光_198027 分钟前
保障性住房管理:SpringBoot技术优势分析
java·spring boot·后端
ajsbxi32 分钟前
苍穹外卖学习记录
java·笔记·后端·学习·nginx·spring·servlet
颜淡慕潇1 小时前
【K8S问题系列 |1 】Kubernetes 中 NodePort 类型的 Service 无法访问【已解决】
后端·云原生·容器·kubernetes·问题解决
尘浮生2 小时前
Java项目实战II基于Spring Boot的光影视频平台(开发文档+数据库+源码)
java·开发语言·数据库·spring boot·后端·maven·intellij-idea
尚学教辅学习资料2 小时前
基于SpringBoot的医药管理系统+LW示例参考
java·spring boot·后端·java毕业设计·医药管理
monkey_meng3 小时前
【Rust中的迭代器】
开发语言·后端·rust
余衫马4 小时前
Rust-Trait 特征编程
开发语言·后端·rust
monkey_meng4 小时前
【Rust中多线程同步机制】
开发语言·redis·后端·rust
W Y4 小时前
【架构-37】Spark和Flink
架构·flink·spark
Gemini19954 小时前
分布式和微服务的区别
分布式·微服务·架构