javascript
讲讲我和Spring创始级程序员共同review代码的故事
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客戶端,它有兩部分組成:
特別說明一下:這個消費客戶端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, 首先他拋出了兩個鏈接:
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文件里的一些標簽的用法并不正確,我來注釋一下,另外,考慮到明年8月Spring Boot 1.X將不再提供支持,所以建議實現直接支持Spring Boot 2.X”。
@Configuration@EnableConfigurationProperties(RocketMQProperties.class)@ConditionalOnClass(MQClientAPIImpl.class)@Order ~~春波特: 這個類里使用Order很不合理呵,不建議使用,完全可以通過其他方式控制runtime是Bean的構建順序@Slf4jpublic class RocketMQAutoConfiguration { @Bean @ConditionalOnClass(DefaultMQProducer.class) ~~春波特: 屬性直接使用類是不科學的,需要用(name=\u0026quot;類全名\u0026quot;) 方式,這樣在類不在classpath時,不會拋出CNFE @ConditionalOnMissingBean(DefaultMQProducer.class) @ConditionalOnProperty(prefix = \u0026quot;spring.rocketmq\u0026quot;, value = {\u0026quot;nameServer\u0026quot;, \u0026quot;producer.group\u0026quot;}) ~~春波特: nameServer屬性名要寫成name-server [1] @Order(1) ~~春波特: 刪掉呵 public DefaultMQProducer mqProducer(RocketMQProperties rocketMQProperties) { ... } @Bean @ConditionalOnClass(ObjectMapper.class) @ConditionalOnMissingBean(name = \u0026quot;rocketMQMessageObjectMapper\u0026quot;) ~~春波特: 不建議與具體的實例名綁定,設計的意圖是使用系統中已經存在的ObjectMapper, 如果沒有,則在這里實例化一個,需要改成 @ConditionalOnMissingBean(ObjectMapper.class) public ObjectMapper rocketMQMessageObjectMapper() { return new ObjectMapper(); } @Bean(destroyMethod = \u0026quot;destroy\u0026quot;) @ConditionalOnBean(DefaultMQProducer.class) @ConditionalOnMissingBean(name = \u0026quot;rocketMQTemplate\u0026quot;) ~~春波特: 與上面一樣 @Order(2) ~~春波特: 刪掉呵 public RocketMQTemplate rocketMQTemplate(DefaultMQProducer mqProducer, @Autowired(required = false) ~~春波特: 刪掉 @Qualifier(\u0026quot;rocketMQMessageObjectMapper\u0026quot;) ~~春波特: 刪掉,不要與具體實例綁定 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 = \u0026quot;spring.rocketmq\u0026quot;, value = \u0026quot;nameServer\u0026quot;) ~~春波特: name-server public static class ListenerContainerConfiguration implements ApplicationContextAware, InitializingBean { ... @Resource ~~春波特: 刪掉這個annotation, 這個field injection的方式不推薦,建議使用setter或者構造參數的方式初始化成員變量 private StandardEnvironment environment; @Autowired(required = false) ~~春波特: 這個注解是不需要的 public ListenerContainerConfiguration( @Qualifier(\u0026quot;rocketMQMessageObjectMapper\u0026quot;) 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里果真有很多學問,羅美琪迅速的調整了代碼,一下看起來清爽了許多。不過還是被春波特提出了兩點建議:
@Configurationpublic class ListenerContainerConfiguration implements ApplicationContextAware, SmartInitializingSingleton { private ObjectMapper objectMapper = new ObjectMapper(); ~~春波特: 性能上考慮,不要初始化這個成員變量,既然這個成員是在構造/setter方法里設置的,就不要在這里初始化,尤其是當它的構造成本很高的時候。 private void registerContainer(String beanName, Object bean) { Class\u0026lt;?\u0026gt; clazz = AopUtils.getTargetClass(bean); if(!RocketMQListener.class.isAssignableFrom(bean.getClass())){ throw new IllegalStateException(clazz + \u0026quot; is not instance of \u0026quot; + 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(\u0026quot;%s_%s\u0026quot;, 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(\u0026quot;started container failed. {}\u0026quot;, container, e); throw new RuntimeException(e); } } ... }}注[3]:使用GenericApplicationContext.registerBean的方式
public final \u0026lt; T \u0026gt; void registerBean(Class\u0026lt; T \u0026gt; beanClass, Supplier\u0026lt; T \u0026gt; 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等領域有深刻理解。
總結
以上是生活随笔為你收集整理的讲讲我和Spring创始级程序员共同review代码的故事的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 加州无人车报告出炉,苹果表现垫底,国产车
- 下一篇: 自底向上的web数据操作指南