做者 | 遼天
來源 | 阿里巴巴雲原生公衆號html
導讀:rocketmq-spring 通過 6 個多月的孵化,做爲 Apache RocketMQ 的子項目正式畢業,發佈了第一個 Release 版本 2.0.1。這個項目是把 RocketMQ 的客戶端使用 Spring Boot 的方式進行了封裝,可讓用戶經過簡單的 annotation 和標準的 Spring Messaging API 編寫代碼來進行消息的發送和消費。java
在項目發佈階段咱們很榮幸的邀請了 Spring 社區的原創人員對咱們的代碼進行了 Review,經過幾輪 slack 上的深刻交流感覺到了 Spring 團隊對開源代碼質量的標準,對 SpringBoot 項目細節的要求。本文是對 Review 和代碼改進過程當中的經驗和技巧的總結,但願從事 Spring Boot 開發的同窗有幫助。咱們把這個過程整理成 RocketMQ 社區的貢獻者羅美琪和 Spring 社區的春波特(SpringBoot)的故事。git
故事的開始是這樣的,羅美琪美眉有一套 RocketMQ 的客戶端代碼,負責發送消息和消費消息。早早的據說春波特小哥哥的大名,經過 Spring Boot 能夠把本身客戶端調用變得很是簡單,只使用一些簡單的註解(annotation)和代碼就可使用獨立應用的方式啓動,省去了複雜的代碼編寫和參數配置。github
聰明的她參考了業界已經實現的消息組件的 Spring 實現了一個 RocketMQ Spring 客戶端:spring
@Resourceprivate RocketMQTemplate rocketMQTemplate; ... SendResult sendResult = rocketMQTemplate.syncSend(xxxTopic, "Hello, World!");
@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 來標註,這個註解的做用有兩個:apache
經過研究發現,Spring-Boot 最核心的實現是自動化配置(auto configuration),它須要分爲三個部分:app
羅美琪美眉按照這個思路開發完成了 RocketMQ SpringBoot 封裝並造成了 starter 交給社區的小夥伴們試用,nice~你們使用後反饋效果不錯。可是仍是想請教一下專業的春波特小哥哥,看看他的意見。異步
春波特小哥哥至關負責地對羅美琪的代碼進行了 Review, 首先他拋出了兩個連接:maven
而後解釋道:ide
「在 Spring Boot 中包含兩個概念 - auto-configuration 和 starter-POMs,它們之間相互關聯,可是不是簡單綁定在一塊兒的:
換句話說,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 特有的幾個要求:
import org.springframework.util.Assert; ... Assert.hasText(nameServer, "[rocketmq.name-server] must not be null");
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); }); }
最後,春波特還至關專業地向羅美琪美眉提供了以下兩方面的意見:
咱們經常使用的代碼註釋分爲多行(/* … /)和單行(// ...)兩種類型,對於須要說明的成員變量,方法或者代碼邏輯應該提供多行註釋; 有些簡單的代碼邏輯註釋也可使用單行註釋。在註釋時通用的要求是首字母大寫開頭,而且使用句號結尾;對於單行註釋,也要求首字母大寫開頭;而且不建議行尾單行註釋。
在變量和方法命名時儘可能用詞準確,而且儘可能不要使用縮寫,如: sendMsgTimeout,建議寫成 sendMessageTimeout;包名 supports,建議改爲 support。
使用 Lombok 的好處是代碼更加簡潔,只須要使用一些註釋就可省略 constructor,setter 和 getter 等諸多方法(bolierplate code);可是也有一個壞處就是須要開發者在本身的 IDE 環境配置 Lombok 插件來支持這一功能,因此 Spring 社區的推薦方式是不使用 Lombok,以便新用戶能夠直接查看和維護代碼,不依賴 IDE 的設置。
若是一個包目錄下沒有任何 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]:下面的截圖是有 FieldInjection 轉變成構造函數設置的代碼示例。
轉換成:
羅美琪根據上述的要求調整了代碼,使代碼質量有了很大的提升,而且總結了 Spring Boot 開發的要點:
經過本次的 Review 工做了解到了 spring-boot 及 auto-configuration 所須要的一些約束條件,信心滿滿地提交了最終的代碼,又能夠邀請 RocketMQ 社區的小夥伴們一塊兒使用 rocketmq-spring 功能了,廣大讀者能夠在參考代碼庫查看到最後修復代碼,也但願有更多的寶貴意見反饋和增強,加油!
開源軟件不只僅是提供一個好用的產品,代碼質量和風格也會影響到廣大的開發者,活躍的社區貢獻者羅美琪還在與 RocketMQ 社區的小夥伴們不斷完善 spring 的代碼,並邀請春波特的 Spring 社區進行宣講和介紹,下一步將 rocketmq-spring-starter 推動到 Spring Initializr,讓用戶能夠直接在 start.spring.io 網站上像使用其它 starter(如: Tomcat starter)同樣使用 rocketmq-spring。