罗美琪和春波特的故事...

简介: 罗美琪和春波特的故事...

作者:辽天

rocketmq-spring经过6个多月的孵化,作为Apache RocketMQ的子项目正式毕业,发布了第一个Release版本2.0.1。这个项目是把RocketMQ的客户端使用Spring Boot的方式进行了封装,可以让用户通过简单的annotation和标准的Spring Messaging API编写代码来进行消息的发送和消费。


在项目发布阶段我们很荣幸的邀请了Spring 社区的原创人员对我们的代码进行了Review,通过几轮slack上的深入交流感受到了Spring团队对开源代码质量的标准,对SpringBoot项目细节的要求。本文是对Review和代码改进过程中的经验和技巧的总结,希望从事Spring Boot开发的同学有帮助。我们把这个过程整理成RocketMQ社区的贡献者罗美琪和Spring社区的春波特(SpringBoot)的故事。


故事的开始



故事的开始是这样的,罗美琪美眉有一套RocketMQ的客户端代码,负责发送消息和消费消息。早早的听说春波特小哥哥的大名,通过Spring Boot可以把自己客户端调用变得非常简单,只使用一些简单的注解(annotation)和代码就可以使用独立应用的方式启动,省去了复杂的代码编写和参数配置。


聪明的她参考了业界已经实现的消息组件的Spring实现了一个RocketMQ Spring客户端:


  1. 需要一个消息的发送客户端,它是一个自动创建的Spring Bean,并且相关属性要能够根据配置文件的配置自动设置, 命名它为: RocketMQTemplate, 同时让它封装发送消息的各种同步和异步的方法。


@Resourceprivate RocketMQTemplate rocketMQTemplate;

...

SendResult sendResult = rocketMQTemplate.syncSend(xxxTopic, "Hello, World!");


2.需要消息的接收客户端,它是一个能够被应用回调的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, 它们之间相互关联,但是不是简单绑定在一起的:

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文件里的一些标签的用法并不正确,帮你注释一下,另外,考虑到Spring官方到明年8月Spring Boot 1.X将不再提供支持,所以建议实现直接支持Spring Boot 2.X"



@Configuration

@EnableConfigurationProperties(RocketMQProperties.class)

@ConditionalOnClass(MQClientAPIImpl.class)

@Order ~~春波特: 这个类里使用Order很不合理呵,不建议使用,完全可以通过其他方式控制runtime是Bean的构建顺序

@Slf4j

publicclassRocketMQAutoConfiguration {

  @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 classListenerContainerConfigurationimplementsApplicationContextAware, 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类,如下示例:

importorg.springframework.util.Assert;

...

Assert.hasText(nameServer, "[rocketmq.name-server]mustnotbenull");


  • Auto Configuration单元测试使用Spring 2.0提供的ApplicationContextRunner


publicclassRocketMQAutoConfigurationTest {

  private ApplicationContextRunner runner = new ApplicationContextRunner()           .withConfiguration(AutoConfigurations.of(RocketMQAutoConfiguration.class));


  @Test(expected = NoSuchBeanDefinitionException.class)   publicvoid testRocketMQAutoConfigurationNotCreatedByDefault() {

      runner.run(context -> context.getBean(RocketMQAutoConfiguration.class));   }

  @Test

  publicvoid 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注解处理器,这样它能够生成辅助元数据文件,加快启动时间。详情见这里(https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-developing-auto-configuration.html#boot-features-custom-starter-module-autoconfigure)。


最后,春波特还相当专业地向罗美琪美眉提供了如下两方面的意见:


通用的规范,好的代码要易读易于维护


  1. 注释与命名规范

我们常用的代码注释分为多行(/** … */)和单行(// ...)两种类型,对于需要说明的成员变量,方法或者代码逻辑应该提供多行注释; 有些简单的代码逻辑注释也可以使用单行注释。在注释时通用的要求是首字母大写开头,并且使用句号结尾;对于单行注释,也要求首字母大写开头; 并且不建议行尾单行注释。

在变量和方法命名时尽量用词准确,并且尽量不要使用缩写,如: sendMsgTimeout, 建议写成sendMessageTimeout;包名supports,建议改成support。


  1. 是否需要使用Lombok

使用Lombok的好处是代码更加简洁,只需要使用一些注释就可省略constructor, setter和getter等诸多方法(bolierplate code);但是也有一个坏处就是需要开发者在自己的IDE环境配置Lombok插件来支持这一功能,所以Spring社区的推荐方式是不使用Lombok,以便新用户可以直接查看和维护代码,不依赖IDE的设置。

  1. 对于包名(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类。

  1. 不建议使用Static Import, 虽然使用它的好处是更少的代码,坏处是破坏程序的可读性和易维护性。


效率,深入代码的细节


  1. static + final method

一个类的static方法不要结合final,除非这个这个类本身是final并且声明private构造(ctor),如果两者结合以为这子类不能再(hiding)定义该方法,给将来的扩展和子类调用带来麻烦

  1. 在配置文件声明的Bean尽量使用构造函数或者Setter方法设置成员变量,而不要使用@Autowared,@Resource等方式注入。[4]
  2. 不要额外初始化无用的成员变量。
  3. 如果一个方法没有任何地方调用,就应该删除;如果一个接口方法不需要,就不要实现这个接口


注[4]: 下面的截图是有FieldInjection转变成构造函数设置的代码示例:

1.png


转换成

2.png


故事的结局



罗美琪根据上述的要求调整了代码,使代码质量有了很大的提高,并且总结了Spring Boot开发的要点:

  1. 编写前参考成熟的spring boot实现代码
  2. 要注意模块的划分,区分autoconfiguration 和 starter
  3. 在编写autoconfiguration Bean的时候,注意@Conditional注解的使用;尽量使用构造器或者setter方法来设置变量,避免使用Field Injection方式;多个Configuration Bean可以使用@Import关联;使用Spring 2.0提供的AutoConfigruation测试类
  4. 注意一些细节: static与BeanPostProcessor; Lifecycle的使用;不必要的成员属性的初始化等


通过本次的Review工作了解到了spring-boot及auto-configuration所需要的一些约束条件,信心满满地提交了最终的代码,又可以邀请RocketMQ社区的小伙伴们一起使用rocketmq-spring功能了,广大读者可以在参考代码库查看到最后修复代码,也希望有更多的宝贵意见反馈和加强,加油!


后记



开源软件不仅仅是提供一个好用的产品,代码质量和风格也会影响到广大的开发者,活跃的社区贡献者罗美琪还在与RocketMQ社区的小伙伴们不断完善spring的代码,并邀请春波特的Spring社区进行宣讲和介绍,下一步将rocketmq-spring-starter推进到Spring Initializr,让用户可以直接在 https://start.aliyun.com/bootstrap.html网站上像使用其它starter(如: Tomcat starter)一样使用rocketmq-spring.

相关实践学习
RocketMQ一站式入门使用
从源码编译、部署broker、部署namesrv,使用java客户端首发消息等一站式入门RocketMQ。
消息队列 MNS 入门课程
1、消息队列MNS简介 本节课介绍消息队列的MNS的基础概念 2、消息队列MNS特性 本节课介绍消息队列的MNS的主要特性 3、MNS的最佳实践及场景应用 本节课介绍消息队列的MNS的最佳实践及场景应用案例 4、手把手系列:消息队列MNS实操讲 本节课介绍消息队列的MNS的实际操作演示 5、动手实验:基于MNS,0基础轻松构建 Web Client 本节课带您一起基于MNS,0基础轻松构建 Web Client
相关文章
|
数据采集 API Python
这个春天,淄博烧烤成了新晋“顶流”
如何使用python爬取抖音热门数据
这个春天,淄博烧烤成了新晋“顶流”
|
消息中间件
【中秋特辑】嫦娥妹妹,你别着急~
今天为大家带来一个小故事,是关于嫦娥和吴刚的爱情故事,想知道吴刚是怎样将情愫传递给嫦娥的吗?让我们一起一探究竟吧~
175 0
【中秋特辑】嫦娥妹妹,你别着急~
|
消息中间件
【中秋特辑】嫦娥妹妹,你别着急~
今天为大家带来一个小故事,是关于嫦娥和吴刚的爱情故事,想知道吴刚是怎样将情愫传递给嫦娥的吗?让我们一起一探究竟吧~
【中秋特辑】嫦娥妹妹,你别着急~
|
机器学习/深度学习
学霸、学神OR开挂
我们学习知识 好比武侠世界里的人修炼武功一般 有人天赋异禀、骨骼清奇 是天生的练武奇才——学神 有人天资平庸,但通过后天的孜孜不倦 终成一代大侠——学霸 还有人一路奇遇不断,屡获高人指点 成为绝世高手——外挂玩家
学霸、学神OR开挂
|
IDE Java 编译器
《C游记》 第一章 - 灵根孕育源流出 初识C言大道生(壹)
《C游记》 第一章 - 灵根孕育源流出 初识C言大道生(壹)
119 0
《C游记》 第一章 - 灵根孕育源流出 初识C言大道生(壹)
|
C语言 C++
《C游记》 第一章 - 灵根孕育源流出 初识C言大道生(贰)
《C游记》 第一章 - 灵根孕育源流出 初识C言大道生(贰)
143 0
《C游记》 第一章 - 灵根孕育源流出 初识C言大道生(贰)
|
消息中间件 IDE Cloud Native
罗美琪和春波特的故事...
rocketmq-spring 经过 6 个多月的孵化,作为 Apache RocketMQ 的子项目正式毕业,发布了第一个 Release 版本 2.0.1。这个项目是把 RocketMQ 的客户端使用 Spring Boot 的方式进行了封装,可以让用户通过简单的 annotation 和标准的 Spring Messaging API 编写代码来进行消息的发送和消费。
罗美琪和春波特的故事...
|
容灾 分布式数据库 数据库
星光不问赶路人,感恩有你 1 路同行!
OceanBase 取得了哪些小成绩?快来跟我一起解锁吧
星光不问赶路人,感恩有你 1 路同行!
(转)阿里八卦:L氓出没,注意!
(转自 http://medic.iteye.com/blog/1056515) 2007年我面试了一个被阿里面试后刷掉的电话销售人员,她本来在广州有一份好好的工作,之所以来杭州就是想进阿里。
814 0
盛夏光年 - 江湖一剑客
何当共剪西窗烛,看巴山上,呦呦鹿鸣。 却话巴山夜雨时,盼君归期,食野之苹。 青青子衿风中飘扬, 悠悠我心雨中凄凉。 但为君故沧海荒, 沉吟至今变田桑。 曾经沧海难为水,沧海月明珠有泪,泪雨零铃终不怨。
1354 0