作者 | 辽天
来源 | 阿里巴巴云原生公众号
在项目发布阶段我们很荣幸的邀请了 Spring 社区的原创人员对我们的代码进行了 Review,通过几轮 slack 上的深入交流感受到了 Spring 团队对开源代码质量的标准,对 SpringBoot 项目细节的要求。本文是对 Review 和代码改进过程中的经验和技巧的总结,希望从事 Spring Boot 开发的同学有帮助。我们把这个过程整理成 RocketMQ 社区的贡献者罗美琪和 Spring 社区的春波特(SpringBoot)的故事。
故事的开始故事的开始是这样的,罗美琪美眉有一套 RocketMQ 的客户端代码,负责发送消息和消费消息。早早的听说春波特小哥哥的大名,通过 Spring Boot 可以把自己客户端调用变得非常简单,只使用一些简单的注解(annotation)和代码就可以使用独立应用的方式启动,省去了复杂的代码编写和参数配置。
聪明的她参考了业界已经实现的消息组件的 Spring 实现了一个 RocketMQ Spring 客户端:
需要一个消息的发送客户端,它是一个自动创建的 Spring Bean,并且相关属性要能够根据配置文件的配置自动设置, 命名它为:RocketMQTemplate, 同时让它封装发送消息的各种同步和异步的方法。@Resourceprivate RocketMQTemplate rocketMQTemplate; SendResult sendResult = rocketMQTemplate.syncSend(xxxTopic, "Hello, World!");需要消息的接收客户端,它是一个能够被应用回调的 Listener, 来将消费消息回调给用户进行相关的处理。
@Service@RocketMQMessageListener(topic = "xxx", consumerGroup = "xxx_consumer") public class StringConsumer implements RocketMQListener String { @Override public void onMessage(String message) { System.out.printf("------- StringConsumer received: %s \n", message); }
特别说明一下:这个消费客户端 Listener 需要通过一个自定义的注解@RocketMQMessageListener 来标注,这个注解的作用有两个:
定义消息消费的配置参数(如: 消费的 topic, 是否顺序消费,消费组等)。?可以让 spring-boot 在启动过程中发现标注了这个注解的所有 Listener, 并进行初始化,详见 ListenerContainerConfiguration 类及其实现 SmartInitializingSingleton 的接口方法 afterSingletonsInstantiated()。通过研究发现,Spring-Boot 最核心的实现是自动化配置(auto configuration),它需要分为三个部分:
AutoConfiguration 类,它由 @Configuration 标注,用来创建 RocketMQ 客户端所需要的 SpringBean,如上面所提到的 RocketMQTemplate 和能够处理消费回调 Listener 的容器,每个 Listener 对应一个容器 SpringBean 来启动 MQPushConsumer,并将来将监听到的消费消息并推送给 Listener 进行回调。可参考 RocketMQAutoConfiguration.java ?(编者注: 这个是最终发布的类,没有 review 的痕迹啦)。上面定义的 Configuration 类,它本身并不会“自动”配置,需要由 META-INF/spring.factories 来声明,可参考 spring.factories 使用这个 META 配置的好处是上层用户不需要关心自动配置类的细节和开关,只要 classpath 中有这个 META-INF 文件和 Configuration 类,即可自动配置。另外,上面定义的 Configuration 类,还定义了 @EnableConfiguraitonProperties 注解来引入 ConfigurationProperties 类,它的作用是定义自动配置的属性,可参考 RocketMQProperties.java,上层用户可以根据这个类里定义的属性来配置相关的属性文件(即 META-INF/application.properties 或 META-INF/application.yaml)。故事的发展罗美琪美眉按照这个思路开发完成了 RocketMQ SpringBoot 封装并形成了 starter 交给社区的小伙伴们试用,nice~大家使用后反馈效果不错。但是还是想请教一下专业的春波特小哥哥,看看他的意见。
春波特小哥哥相当负责地对罗美琪的代码进行了 Review, 首先他抛出了两个链接:
https://github.com/spring-projects/spring-boot/wiki/Building-On-Spring-Boot?https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-developing-auto-configuration.html然后解释道:
“在 Spring Boot 中包含两个概念 - auto-configuration 和 starter-POMs,它们之间相互关联,但是不是简单绑定在一起的:
auto-configuration 负责响应应用程序的当前状态并配置适当的 Spring Bean。它放在用户的 CLASSPATH 中结合在 CLASSPATH 中的其它依赖就可以提供相关的功能。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 文件里的一些标签的用法并不正确,帮你注释一下,另外,考虑到 Spring 官方到 2020 年 8 月 Spring Boot 1.X 不再提供支持,所以建议实现直接支持 Spring Boot 2.X。”
@Configuration @EnableConfigurationProperties(RocketMQProperties.class) @ConditionalOnClass(MQClientAPIImpl.class) @Order ~~春波特: 这个类里使用Order很不合理呵,不建议使用,完全可以通过其他方式控制runtime是Bean的构建顺序 @Slf4j public class RocketMQAutoConfiguration { @Bean @ConditionalOnClass(DefaultMQProducer.class) ~~春波特: 属性直接使用类是不科学的,需要用(name="类全名") 方式,这样在类不在classpath时,不会抛出CNFE @ConditionalOnMissingBean(DefaultMQProducer.class) @ConditionalOnProperty(prefix = "spring.rocketmq", value = {"nameServer", "producer.group"}) ~~春波特: nameServer属性名要写成name-server [1] @Order(1) ~~春波特: 删掉呵 public DefaultMQProducer mqProducer(RocketMQProperties rocketMQProperties) { @Bean @ConditionalOnClass(ObjectMapper.class) @ConditionalOnMissingBean(name = "rocketMQMessageObjectMapper") ~~春波特: 不建议与具体的实例名绑定,设计的意图是使用系统中已经存在的ObjectMapper, 如果没有,则在这里实例化一个,需要改成 @ConditionalOnMissingBean(ObjectMapper.class) public ObjectMapper rocketMQMessageObjectMapper() { return new ObjectMapper(); @Bean(destroyMethod = "destroy") @ConditionalOnBean(DefaultMQProducer.class) @ConditionalOnMissingBean(name = "rocketMQTemplate") ~~春波特: 与上面一样 @Order(2) ~~春波特: 删掉呵 public RocketMQTemplate rocketMQTemplate(DefaultMQProducer mqProducer, @Autowired(required = false) ~~春波特: 删掉 @Qualifier("rocketMQMessageObjectMapper") ~~春波特: 删掉,不要与具体实例绑定 ObjectMapper objectMapper) { RocketMQTemplate rocketMQTemplate = new RocketMQTemplate(); rocketMQTemplate.setProducer(mqProducer); if (Objects.nonNull(objectMapper)) { rocketMQTemplate.setObjectMapper(objectMapper); return rocketMQTemplate; @Bean(name = RocketMQConfigUtils.ROCKETMQ_TRANSACTION_ANNOTATION_PROCESSOR_BEAN_NAME) @ConditionalOnBean(TransactionHandlerRegistry.class) @Role(BeanDefinition.ROLE_INFRASTRUCTURE) ~~春波特: 这个bean(RocketMQTransactionAnnotationProcessor)建议声明成static的,因为这个RocketMQTransactionAnnotationProcessor实现了BeanPostProcessor接口,接口里方法在调用的时候(创建Transaction相关的Bean的时候)可以直接使用这个static实例,而不要等到这个Configuration类的其他的Bean都构建好 [2] public RocketMQTransactionAnnotationProcessor transactionAnnotationProcessor( TransactionHandlerRegistry transactionHandlerRegistry) { return new RocketMQTransactionAnnotationProcessor(transactionHandlerRegistry); @Configuration ~~春波特: 这个内嵌的Configuration类比较复杂,建议独立成一个顶级类,并且使用 @Import在主Configuration类中引入 @ConditionalOnClass(DefaultMQPushConsumer.class) @EnableConfigurationProperties(RocketMQProperties.class) @ConditionalOnProperty(prefix = "spring.rocketmq", value = "nameServer") ~~春波特: name-server public static class ListenerContainerConfiguration implements ApplicationContextAware, InitializingBean { @Resource ~~春波特: 删掉这个annotation, 这个field injection的方式不推荐,建议使用setter或者构造参数的方式初始化成员变量 private StandardEnvironment environment; @Autowired(required = false) ~~春波特: 这个注解是不需要的 public ListenerContainerConfiguration( @Qualifier("rocketMQMessageObjectMapper") ObjectMapper objectMapper) { ~~春波特: @Qualifier 不需要 this.objectMapper = objectMapper; }
注[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 里果真很有学问,罗美琪迅速的调整了代码,一下看起来清爽了许多。不过还是被春波特提出了两点建议:
@Configuration public class ListenerContainerConfiguration implements ApplicationContextAware, SmartInitializingSingleton { private ObjectMapper objectMapper = new ObjectMapper(); ~~春波特: 性能上考虑,不要初始化这个成员变量,既然这个成员是在构造/setter方法里设置的,就不要在这里初始化,尤其是当它的构造成本很高的时候。 private void registerContainer(String beanName, Object bean) { Class ? clazz = AopUtils.getTargetClass(bean); if(!RocketMQListener.class.isAssignableFrom(bean.getClass())){ throw new IllegalStateException(clazz + " is not instance of " + RocketMQListener.class.getName()); RocketMQListener rocketMQListener = (RocketMQListener) bean; RocketMQMessageListener annotation = clazz.getAnnotation(RocketMQMessageListener.class); validate(annotation); ~~春波特: 下面的这种手工注册Bean的方式是Spring 4.x里提供能,可以考虑使用Spring5.0 里提供的 GenericApplicationContext.registerBean的方法,通过supplier调用new来构造Bean实例 [3] BeanDefinitionBuilder beanBuilder = BeanDefinitionBuilder.rootBeanDefinition(DefaultRocketMQListenerContainer.class); beanBuilder.addPropertyValue(PROP_NAMESERVER, rocketMQProperties.getNameServer()); beanBuilder.setDestroyMethodName(METHOD_DESTROY); String containerBeanName = String.format("%s_%s", DefaultRocketMQListenerContainer.class.getName(), counter.incrementAndGet()); DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory) applicationContext.getBeanFactory(); beanFactory.registerBeanDefinition(containerBeanName, beanBuilder.getBeanDefinition()); DefaultRocketMQListenerContainer container = beanFactory.getBean(containerBeanName, DefaultRocketMQListenerContainer.class); ~~春波特: 你这里的启动方法是通过 afterPropertiesSet() 调用的,这个是不建议的,应该实现SmartLifecycle来定义启停方法,这样在ApplicationContext刷新时能够自动启动;并且避免了context初始化时由于底层资源问题导致的挂住(stuck)的危险 if (!container.isStarted()) { try { container.start(); } catch (Exception e) { log.error("started container failed. {}", container, e); throw new RuntimeException(e); }
注[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 类,如下示例:import org.springframework.util.Assert; Assert.hasText(nameServer, "[rocketmq.name-server] must not be null");Auto Configuration 单元测试使用 Spring 2.0 提供的 ApplicationContextRunner:
public class RocketMQAutoConfigurationTest { private ApplicationContextRunner runner = new ApplicationContextRunner() .withConfiguration(AutoConfigurations.of(RocketMQAutoConfiguration.class)); @Test(expected = NoSuchBeanDefinitionException.class) public void testRocketMQAutoConfigurationNotCreatedByDefault() { runner.run(context - context.getBean(RocketMQAutoConfiguration.class)); } @Test public void testDefaultMQProducerWithRelaxPropertyName() { runner.withPropertyValues("rocketmq.name-server=127.0.0.1:9876", "rocketmq.producer.group=spring_rocketmq"). run((context) - { assertThat(context).hasSingleBean(DefaultMQProducer.class); assertThat(context).hasSingleBean(RocketMQProperties.class); }); }在 auto-configuration 模块的 pom.xml 文件里,加入 spring-boot-configuration-processor 注解处理器,这样它能够生成辅助元数据文件,加快启动时间。
最后,春波特还相当专业地向罗美琪美眉提供了如下两方面的意见:
1. 通用的规范,好的代码要易读易于维护1)注释与命名规范我们常用的代码注释分为多行(/ … /)和单行(// ...)两种类型,对于需要说明的成员变量,方法或者代码逻辑应该提供多行注释; 有些简单的代码逻辑注释也可以使用单行注释。在注释时通用的要求是首字母大写开头,并且使用句号结尾;对于单行注释,也要求首字母大写开头*;并且不建议行尾单行注释。
在变量和方法命名时尽量用词准确,并且尽量不要使用缩写,如: sendMsgTimeout,建议写成 sendMessageTimeout;包名 supports,建议改成 support。
2)是否需要使用?Lombok使用 Lombok 的好处是代码更加简洁,只需要使用一些注释就可省略 constructor,setter 和 getter 等诸多方法(bolierplate code);但是也有一个坏处就是需要开发者在自己的 IDE 环境配置 Lombok 插件来支持这一功能,所以 Spring 社区的推荐方式是不使用 Lombok,以便新用户可以直接查看和维护代码,不依赖 IDE 的设置。
3)对于包名(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 类。
4)不建议使用 Static Import, 虽然使用它的好处是更少的代码,坏处是破坏程序的可读性和易维护性。2. 效率,深入代码的细节static + final method:一个类的 static 方法不要结合 final,除非这个类本身是 final 并且声明 private 构造(ctor),如果两者结合以为这子类不能再(hiding)定义该方法,给将来的扩展和子类调用带来麻烦。在配置文件声明的 Bean 尽量使用构造函数或者 Setter 方法设置成员变量,而不要使用@Autowared,@Resource等方式注入。不要额外初始化无用的成员变量。如果一个方法没有任何地方调用,就应该删除;如果一个接口方法不需要,就不要实现这个接口类。注[4]:下面的截图是有 FieldInjection 转变成构造函数设置的代码示例。
转换成:
故事的结局罗美琪根据上述的要求调整了代码,使代码质量有了很大的提高,并且总结了 Spring Boot 开发的要点:
编写前参考成熟的 spring boot 实现代码。要注意模块的划分,区分 autoconfiguration 和 starter。在编写 autoconfiguration Bean 的时候,注意 @Conditional 注解的使用;尽量使用构造器或者 setter 方法来设置变量,避免使用 Field Injection 方式;多个 Configuration Bean 可以使用 @Import 关联;使用 Spring 2.0 提供的 AutoConfigruation 测试类。注意一些细节:static 与 BeanPostProcessor;Lifecycle 的使用;不必要的成员属性的初始化等。通过本次的 Review 工作了解到了 spring-boot 及 auto-configuration 所需要的一些约束条件,信心满满地提交了最终的代码,又可以邀请 RocketMQ 社区的小伙伴们一起使用 rocketmq-spring 功能了,广大读者可以在参考代码库查看到最后修复代码,也希望有更多的宝贵意见反馈和加强,加油!
后记开源软件不仅仅是提供一个好用的产品,代码质量和风格也会影响到广大的开发者,活跃的社区贡献者罗美琪还在与 RocketMQ 社区的小伙伴们不断完善 spring 的代码,并邀请春波特的 Spring 社区进行宣讲和介绍,下一步将 rocketmq-spring-starter 推进到 Spring Initializr,让用户可以直接在 start.spring.io 网站上像使用其它 starter(如: Tomcat starter)一样使用 rocketmq-spring。
钉钉搜索群号:21982288,即可加入 Apache RocketMQ 中国开发者官方钉钉群!
在 PC 端登录 start.aliyun.com 知行动手实验室,沉浸式体验在线交互教程。
2020数据猿年度金猿主题策划活动》之2020大数据产业最具投资价值企业榜丨数据猿...
从 C++ 转 Python 的时候,我已经是一个有四年全职工作经验的软件开发者了。我的...
教育数据中台解决方案为教育行业的客户提供一站式数据汇聚、治理加工、质量保证...
在手机上相信都有来自服务器的推送消息,比如一些及时的新闻信息,这篇文章主要...
本文转载自微信公众号「程序新视界 」,作者二师兄。转载本文请联系程序新视界 ...
一、什么是实时计算 Flink 版? 实时计算 Flink 版(Alibaba Cloud Realtime Com...
最近有同学面了快手,分享一波面经~ 快手前端实习(一、二)面经(3.28) 一面: 1...
本文转载自公众号读芯术(ID:AI_Discovery) 拿破仑有句名言:战争需要三样东西钱...
本文转载自公众号读芯术(ID:AI_Discovery) 下面这个模型在一项图像识别竞赛中经...
大多数企业仍在将工作负载从数据中心迁移到云端。尽管通常省钱不是这种迁移的主...