关于云原生:罗美琪和春波特的故事

39次阅读

共计 11471 个字符,预计需要花费 29 分钟才能阅读完成。

作者 | 辽天
起源 | 阿里巴巴云原生公众号

导读: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 客户端:

  • 须要一个音讯的发送客户端,它是一个主动创立的 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-pro…
  • https://docs.spring.io/spring…

而后解释道:

“在 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 注解处理器,这样它可能生成辅助元数据文件,放慢启动工夫。

详情见这里:
https://docs.spring.io/spring…

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

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 知口头手实验室,沉迷式体验在线交互教程

正文完
 0