RocketMQ-Spring 毕业了。
作为 Apache RocketMQ 的子项目,经过 6 个多月的孵化,RocketMQ-Spring 发布了第一个 Release 版本 v2.0.1,通过使用 Spring Boot 的方式把 RocketMQ 的客户端进行封装,帮助用户通过简单的 Annotation 和标准的 Spring Messaging API 编写代码,来进行消息的发送和消费,以降低开发复杂度。
本文将以故事的形式,还原RocketMQ社区开发者和Spring社区创始工程师一同Review代码以及对代码进行改进的全过程,希望对做Spring Boot开发的同学有所帮助。
罗美琪:RocketMQ 社区开发者
春波特小哥:Spring 社区创始工程师
故事的开始
罗美琪有一套 RocketMQ 的客户端代码,负责发送消息和消费消息。听说春波特小哥善于消息发送,通过 Spring Boot 可以把自己客户端调用变得非常简单,只需要一些简单的注解(Annotation)和代码就可以使用独立应用的方式来启动,省去了复杂的代码编写和参数配置。
罗美琪参考了业界已经实现的消息组件的 Spring 实现了一个 RocketMQ Spring 客户端,它有两部分组成:
消息的发送客户端:这是一个自动创建的Spring Bean,相关属性能够根据配置文件的配置自动设置,命名它为: RocketMQTemplate, 同时用来封装发送消息的各种同步和异步的方法。
消息的接收客户端:这是一个能够被应用回调的Listener, 用于将消费消息回调给用户进行相关的处理。
特别说明一下:这个消费客户端 Listener 需要通过一个自定义的注解 @RocketMQMessageListener 来标注,这个注解的作用有两个:
定义消息消费的配置参数(如: 消费的Topic, 是否顺序消费,消费组等);
可以让spring-boot在启动过程中发现标注了这个注解的所有Listener,并进行初始化,详见ListenerContainerConfiguration类及其实现SmartInitializingSingleton的接口方法afterSingletonsInstantiated()。
罗美琪发现,Spring-Boot 最核心的实现是自动化配置(Auto Configuration),它分为三个部分:
由@Configuration标注,用来创建RocketMQ客户端所需要的SpringBean,如上面所提到的RocketMQTemplate和能够处理消费回调Listener的容器,每个Listener对应一个容器SpringBean,来启动MQPushConsumer,并能将监听到的消费消息推送给Listener进行回调。参考:RocketMQAutoConfiguration.java (编者注: 这个是最终发布的类,没有review的痕迹)
实现“自动”配置,还需要由META-INF/spring.factories来声明。参考:spring.factories。使用这个META配置的好处是上层用户不需要关心自动配置类的细节和开关,只要classpath中有这个META-INF文件和Configuration类,就能实现自动配置。
定义了@EnableConfiguraitonProperties注解,来引入ConfigurationProperties类,它的作用是定义自动配置的属性。参考:RocketMQProperties.java。上层用户可以根据这个类里定义的属性,配置相关的属性文件(即 META-INF/application.properties 或 META-INF/application.yaml)
故事的发展
罗美琪按照这个思路完成了 RocketMQ SpringBoot 的封装并形成了 starter,提交给社区的小伙伴们试用,nice,大家使用后反馈效果不错。但是还是想请教一下专业的春波特小哥哥,看看他的建议。
春波特小哥相当的负责地对罗美琪的代码进行了 Review, 首先他抛出了两个链接:
然后解释道:在 Spring Boot 中包含两个概念: auto-configuration 和 starter-POMs, 它们之间相互关联,但并非简单绑定在一起的:
a. auto-configuration 负责响应应用程序的当前状态,并配置适当的 Spring Bean。它放在用户的 CLASSPATH 中,结合在 CLASSPATH 中的其它依赖,就可以提供相关的功能;
b. Starter-POM 负责把 auto-configuration 和一些附加的依赖组织在一起,提供开箱即用的功能,它通常是一个 maven project, 里面只是一个 POM 文件,不需要包含任何附加的 classes 或 resources;
“换句话说,starter-POM负责配置全量的classpath, 而auto-configuration负责具体的响应(实现);前者是total-solution, 后者可以按需使用。你现在的系统是单一的一个module把auto-configuration和starter-POM混在了一起,这个不利于以后的扩展和模块的单独使用。”
罗美琪明白区分对项目维护的重要性,于是将代码进行了模块化:
rocketmq-spring-boot-parent:父POM
rocketmq-spring-boot:auto-configuraiton模块
rocketmq-spring-stater:starter模块 (实际上只包含一个pom.xml文件)
rocketmq-spring-samples:调用starter的示例样本
“很好,这样的模块结构就清晰多了”,春波特小哥哥点头,“但是这个AutoConfiguration文件里的一些标签的用法并不正确,我来注释一下,另外,考虑到明年8月Spring Boot 1.X将不再提供支持,所以建议实现直接支持Spring Boot 2.X”。
注[1]:在声明属性的时候不要使用驼峰命名法,要使用-横线分隔,这样才能支持属性名的松散规则(relaxed rules)。
注[2]:BeanPostProcessor接口作用是:如果需要在Spring容器完成Bean的实例化、配置和其他的初始化的前后添加一些自己的逻辑处理,就可以定义一个或者多个BeanPostProcessor接口的实现,然后注册到容器中。为什么建议声明成static的,春波特的英文原文如下:
If they don’t we basically register the post-processor at the same “time” as all the other beans in that class and the contract of BPP is that it must be registered very early on. This may not make a difference for this particular class but flagging it as static as the side effect to make clear your BPP implementation is not supposed to drag other beans via dependency injection.
AutoConfiguration 里果真有很多学问,罗美琪迅速的调整了代码,一下看起来清爽了许多。不过还是被春波特提出了两点建议:
注[3]:使用GenericApplicationContext.registerBean的方式
public final < T > void registerBean(Class< T > beanClass, Supplier< T > supplier, BeanDefinitionCustomizer… ustomizers)
“还有,还有”,罗美琪按照春波特的建议调整完代码后,春波特哥哥提出了 Spring Boot 特有的几个要求:
使用Spring的Assert在传统的Java代码中我们使用assert进行断言,Spring Boot中断言需要使用它自有的Assert类,如下示例:
Auto Configuration单元测试使用Spring 2.0提供 ApplicationContextRunner
在 auto-configuration 模块的 pom.xml 文件里,加入 spring-boot-configuration-processor 注解处理器。这样它能够生成辅助元数据文件,加快启动时间。详情见这里(https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-developing-auto-configuration.html#boot-features-custom-starter-module-autoconfigure)。
最后,春波特还向罗美琪分享了一些实践经验:
通用的规范,好的代码要易读易于维护
a. 注释与命名规范
我们常用的代码注释分为多行(/** … */)和单行(// …)两种类型,对于需要说明的成员变量,方法或者代码逻辑应该提供多行注释; 有些简单的代码逻辑注释也可以使用单行注释。在注释时通用的要求是首字母大写开头,并且使用句号结尾;对于单行注释,也要求首字母大写开头; 并且不建议行尾单行注释。
在变量和方法命名时尽量用词准确,并且尽量不要使用缩写,如: sendMsgTimeout, 建议写成 sendMessageTimeout;包名 supports,建议改成 support。
**b. 是否需要使用 Lombok **
使用 Lombok 的好处是代码更加简洁,只需要使用一些注释就可省略 constructor, setter 和 getter 等诸多方法(bolierplate code);但是也有一个坏处就是需要开发者在自己的 IDE 环境配置 Lombok 插件来支持这一功能,所以 Spring 社区的推荐方式是不使用 Lombok,以便新用户可以直接查看和维护代码,不依赖 IDE 的设置。
c. 对于包名(package)的控制
如果一个包目录下没有任何 class,建议要去掉这个包目录。例如:org.apache.rocketmq.spring.starter 在 spring 目录下没有具体的 class 定义,那么应该去掉这层目录(编者注: 我们最终把 package 改为 org.apache.rocketmq.spring,将 starter 下的目录和 classes 上移一层)。
我们把所有 Enum 类放在包 org.apache.rocketmq.spring.enums 下,这个包命名并不规范,需要把 Enum 类调整到具体的包中,去掉 enums 包;类的隐藏,对于有些类,它只被包中的其它类使用,而不需要把具体的使用细节暴漏给最终用户,建议使用 package private 约束,例如: TransactionHandler 类。
d. 不建议使用 Static Import
虽然使用它的好处是更少的代码,坏处是破坏程序的可读性和易维护性。
效率,深入代码的细节
a. static + final method,一个类的 static 方法不要结合 final,除非这个这个类本身是 final 并且声明 private 构造(ctor),如果两者结合以为这子类不能再(hiding)定义该方法,给将来的扩展和子类调用带来麻烦。
b. 在配置文件声明的 Bean 尽量使用构造函数或者 Setter 方法设置成员变量,而不要使用 @Autowared,@Resource 等方式注入。[4]
c. 不要额外初始化无用的成员变量。
d. 如果一个方法没有任何地方调用,就应该删除;如果一个接口方法不需要,就不要实现这个接口类
注[4]:下面的截图是有 FieldInjection 转变成构造函数设置的代码示例:
转换成
故事的结局
罗美琪按照春波特小哥的建议,进一步调整了代码,大幅度提高了代码质量,并且总结了 Spring Boot 开发的要点:
a. 编写前参考成熟的 spring boot 实现代码;
b. 要注意模块的划分,区分 autoconfiguration 和 starter;
c. 在编写 autoconfiguration Bean 的时候,注意 @Conditional 注解的使用;尽量使用构造器或者 setter 方法来设置变量,避免使用 Field Injection 方式;多个 Configuration Bean 可以使用 @Import 关联;使用 Spring 2.0 提供的 AutoConfigruation 测试类;
d. 注意一些细节: static 与 BeanPostProcessor; Lifecycle 的使用;不必要的成员属性的初始化等;
后记
开源软件不仅要关注产品的易用性,更要在乎代码质量和代码风格。
活跃的社区贡献者罗美琪继续在与 RocketMQ 社区的小伙伴们不断完善 Spring 的代码,并邀请春波特的 Spring 社区进行更多的技术分享。下一步他们将 rocketmq-spring-starter 推进到 Spring Initializr,让用户可以在 start.spring.io 上像使用其它 starter(如: Tomcat starter)一样使用 rocketmq-spring。
作者简介
辽天,社区 ID walking98,阿里巴巴技术专家,Apache RocketMQ 内核控,拥有多年分布式系统研发经验,对 Microsoft Messaging、Storage 等领域有深刻理解。
评论 1 条评论