罗美琪和春波特的故事...
作者 | 遼天
來(lái)源 | 阿里巴巴云原生公眾號(hào)
**導(dǎo)讀:**rocketmq-spring 經(jīng)過(guò) 6 個(gè)多月的孵化,作為 Apache RocketMQ 的子項(xiàng)目正式畢業(yè),發(fā)布了第一個(gè) Release 版本 2.0.1。這個(gè)項(xiàng)目是把 RocketMQ 的客戶端使用 Spring Boot 的方式進(jìn)行了封裝,可以讓用戶通過(guò)簡(jiǎn)單的 annotation 和標(biāo)準(zhǔn)的 Spring Messaging API 編寫(xiě)代碼來(lái)進(jìn)行消息的發(fā)送和消費(fèi)。
在項(xiàng)目發(fā)布階段我們很榮幸的邀請(qǐng)了 Spring 社區(qū)的原創(chuàng)人員對(duì)我們的代碼進(jìn)行了 Review,通過(guò)幾輪 slack 上的深入交流感受到了 Spring 團(tuán)隊(duì)對(duì)開(kāi)源代碼質(zhì)量的標(biāo)準(zhǔn),對(duì) SpringBoot 項(xiàng)目細(xì)節(jié)的要求。本文是對(duì) Review 和代碼改進(jìn)過(guò)程中的經(jīng)驗(yàn)和技巧的總結(jié),希望從事 Spring Boot 開(kāi)發(fā)的同學(xué)有幫助。我們把這個(gè)過(guò)程整理成 RocketMQ 社區(qū)的貢獻(xiàn)者羅美琪和 Spring 社區(qū)的春波特(SpringBoot)的故事。
故事的開(kāi)始
故事的開(kāi)始是這樣的,羅美琪美眉有一套 RocketMQ 的客戶端代碼,負(fù)責(zé)發(fā)送消息和消費(fèi)消息。早早的聽(tīng)說(shuō)春波特小哥哥的大名,通過(guò) Spring Boot 可以把自己客戶端調(diào)用變得非常簡(jiǎn)單,只使用一些簡(jiǎn)單的注解(annotation)和代碼就可以使用獨(dú)立應(yīng)用的方式啟動(dòng),省去了復(fù)雜的代碼編寫(xiě)和參數(shù)配置。
聰明的她參考了業(yè)界已經(jīng)實(shí)現(xiàn)的消息組件的 Spring 實(shí)現(xiàn)了一個(gè) RocketMQ Spring 客戶端:
- 需要一個(gè)消息的發(fā)送客戶端,它是一個(gè)自動(dòng)創(chuàng)建的 Spring Bean,并且相關(guān)屬性要能夠根據(jù)配置文件的配置自動(dòng)設(shè)置, 命名它為:RocketMQTemplate, 同時(shí)讓它封裝發(fā)送消息的各種同步和異步的方法。
- 需要消息的接收客戶端,它是一個(gè)能夠被應(yīng)用回調(diào)的 Listener, 來(lái)將消費(fèi)消息回調(diào)給用戶進(jìn)行相關(guān)的處理。
特別說(shuō)明一下:這個(gè)消費(fèi)客戶端 Listener 需要通過(guò)一個(gè)自定義的注解@RocketMQMessageListener 來(lái)標(biāo)注,這個(gè)注解的作用有兩個(gè):
- 定義消息消費(fèi)的配置參數(shù)(如: 消費(fèi)的 topic, 是否順序消費(fèi),消費(fèi)組等)。
- 可以讓 spring-boot 在啟動(dòng)過(guò)程中發(fā)現(xiàn)標(biāo)注了這個(gè)注解的所有 Listener, 并進(jìn)行初始化,詳見(jiàn) ListenerContainerConfiguration 類及其實(shí)現(xiàn) SmartInitializingSingleton 的接口方法 afterSingletonsInstantiated()。
通過(guò)研究發(fā)現(xiàn),Spring-Boot 最核心的實(shí)現(xiàn)是自動(dòng)化配置(auto configuration),它需要分為三個(gè)部分:
- AutoConfiguration 類,它由 @Configuration 標(biāo)注,用來(lái)創(chuàng)建 RocketMQ 客戶端所需要的 SpringBean,如上面所提到的 RocketMQTemplate 和能夠處理消費(fèi)回調(diào) Listener 的容器,每個(gè) Listener 對(duì)應(yīng)一個(gè)容器 SpringBean 來(lái)啟動(dòng) MQPushConsumer,并將來(lái)將監(jiān)聽(tīng)到的消費(fèi)消息并推送給 Listener 進(jìn)行回調(diào)。可參考 RocketMQAutoConfiguration.java ?(編者注: 這個(gè)是最終發(fā)布的類,沒(méi)有 review 的痕跡啦)。
- 上面定義的 Configuration 類,它本身并不會(huì)“自動(dòng)”配置,需要由 META-INF/spring.factories 來(lái)聲明,可參考 spring.factories 使用這個(gè) META 配置的好處是上層用戶不需要關(guān)心自動(dòng)配置類的細(xì)節(jié)和開(kāi)關(guān),只要 classpath 中有這個(gè) META-INF 文件和 Configuration 類,即可自動(dòng)配置。
- 另外,上面定義的 Configuration 類,還定義了 @EnableConfiguraitonProperties 注解來(lái)引入 ConfigurationProperties 類,它的作用是定義自動(dòng)配置的屬性,可參考 RocketMQProperties.java,上層用戶可以根據(jù)這個(gè)類里定義的屬性來(lái)配置相關(guān)的屬性文件(即 META-INF/application.properties 或 META-INF/application.yaml)。
故事的發(fā)展
羅美琪美眉按照這個(gè)思路開(kāi)發(fā)完成了 RocketMQ SpringBoot 封裝并形成了 starter 交給社區(qū)的小伙伴們?cè)囉?#xff0c;nice~大家使用后反饋效果不錯(cuò)。但是還是想請(qǐng)教一下專業(yè)的春波特小哥哥,看看他的意見(jiàn)。
春波特小哥哥相當(dāng)負(fù)責(zé)地對(duì)羅美琪的代碼進(jìn)行了 Review, 首先他拋出了兩個(gè)鏈接:
- 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 中包含兩個(gè)概念 - auto-configuration 和 starter-POMs,它們之間相互關(guān)聯(lián),但是不是簡(jiǎn)單綁定在一起的:
- auto-configuration 負(fù)責(zé)響應(yīng)應(yīng)用程序的當(dāng)前狀態(tài)并配置適當(dāng)?shù)?Spring Bean。它放在用戶的 CLASSPATH 中結(jié)合在 CLASSPATH 中的其它依賴就可以提供相關(guān)的功能。
- Starter-POM 負(fù)責(zé)把 auto-configuration 和一些附加的依賴組織在一起,提供開(kāi)箱即用的功能,它通常是一個(gè) maven project,里面只是一個(gè) POM 文件,不需要包含任何附加的 classes 或 resources。
換句話說(shuō),starter-POM 負(fù)責(zé)配置全量的 classpath,而 auto-configuration 負(fù)責(zé)具體的響應(yīng)(實(shí)現(xiàn));前者是 total-solution,后者可以按需使用。
你現(xiàn)在的系統(tǒng)是單一的一個(gè) module 把 auto-configuration 和 starter-POM 混在了一起,這個(gè)不利于以后的擴(kuò)展和模塊的單獨(dú)使用。”
羅美琪了解到了區(qū)分確實(shí)對(duì)日后的項(xiàng)目維護(hù)很重要,于是將代碼進(jìn)行了模塊化:
|— rocketmq-spring-boot-parent ?父 POM
|— rocketmq-spring-boot ?????????????auto-configuraiton 模塊
|— rocketmq-spring-stater ??????????starter 模塊(實(shí)際上只包含一個(gè) pom.xml 文件)
|— rocketmq-spring-samples ?????? ?調(diào)用 starter 的示例樣本
“很好,這樣的模塊結(jié)構(gòu)就清晰多了”,春波特小哥哥點(diǎn)頭,“但是這個(gè) AutoConfiguration 文件里的一些標(biāo)簽的用法并不正確,幫你注釋一下,另外,考慮到 Spring 官方到 2020 年 8 月 Spring Boot 1.X 不再提供支持,所以建議實(shí)現(xiàn)直接支持 Spring Boot 2.X。”
@Configuration @EnableConfigurationProperties(RocketMQProperties.class) @ConditionalOnClass(MQClientAPIImpl.class) @Order ~~春波特: 這個(gè)類里使用Order很不合理呵,不建議使用,完全可以通過(guò)其他方式控制runtime是Bean的構(gòu)建順序 @Slf4j public class RocketMQAutoConfiguration {@Bean@ConditionalOnClass(DefaultMQProducer.class) ~~春波特: 屬性直接使用類是不科學(xué)的,需要用(name="類全名") 方式,這樣在類不在classpath時(shí),不會(huì)拋出CNFE@ConditionalOnMissingBean(DefaultMQProducer.class)@ConditionalOnProperty(prefix = "spring.rocketmq", value = {"nameServer", "producer.group"}) ~~春波特: nameServer屬性名要寫(xiě)成name-server [1]@Order(1) ~~春波特: 刪掉呵 public DefaultMQProducer mqProducer(RocketMQProperties rocketMQProperties) {...}@Bean@ConditionalOnClass(ObjectMapper.class)@ConditionalOnMissingBean(name = "rocketMQMessageObjectMapper") ~~春波特: 不建議與具體的實(shí)例名綁定,設(shè)計(jì)的意圖是使用系統(tǒng)中已經(jīng)存在的ObjectMapper, 如果沒(méi)有,則在這里實(shí)例化一個(gè),需要改成@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") ~~春波特: 刪掉,不要與具體實(shí)例綁定 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) ~~春波特: 這個(gè)bean(RocketMQTransactionAnnotationProcessor)建議聲明成static的,因?yàn)檫@個(gè)RocketMQTransactionAnnotationProcessor實(shí)現(xiàn)了BeanPostProcessor接口,接口里方法在調(diào)用的時(shí)候(創(chuàng)建Transaction相關(guān)的Bean的時(shí)候)可以直接使用這個(gè)static實(shí)例,而不要等到這個(gè)Configuration類的其他的Bean都構(gòu)建好 [2]public RocketMQTransactionAnnotationProcessor transactionAnnotationProcessor( TransactionHandlerRegistry transactionHandlerRegistry) {return new RocketMQTransactionAnnotationProcessor(transactionHandlerRegistry);}@Configuration ~~春波特: 這個(gè)內(nèi)嵌的Configuration類比較復(fù)雜,建議獨(dú)立成一個(gè)頂級(jí)類,并且使用@Import在主Configuration類中引入 @ConditionalOnClass(DefaultMQPushConsumer.class)@EnableConfigurationProperties(RocketMQProperties.class)@ConditionalOnProperty(prefix = "spring.rocketmq", value = "nameServer") ~~春波特: name-serverpublic static class ListenerContainerConfiguration implements ApplicationContextAware, InitializingBean {...@Resource ~~春波特: 刪掉這個(gè)annotation, 這個(gè)field injection的方式不推薦,建議使用setter或者構(gòu)造參數(shù)的方式初始化成員變量private StandardEnvironment environment;@Autowired(required = false) ~~春波特: 這個(gè)注解是不需要的public ListenerContainerConfiguration(@Qualifier("rocketMQMessageObjectMapper") ObjectMapper objectMapper) { ~~春波特: @Qualifier 不需要this.objectMapper = objectMapper;}注[1]:在聲明屬性的時(shí)候不要使用駝峰命名法,要使用-橫線分隔,這樣才能支持屬性名的松散規(guī)則(relaxed rules)。
注[2]:BeanPostProcessor 接口作用是:如果需要在 Spring 容器完成 Bean 的實(shí)例化、配置和其他的初始化的前后添加一些自己的邏輯處理,就可以定義一個(gè)或者多個(gè) BeanPostProcessor 接口的實(shí)現(xiàn),然后注冊(cè)到容器中。為什么建議聲明成 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 里果真很有學(xué)問(wèn),羅美琪迅速的調(diào)整了代碼,一下看起來(lái)清爽了許多。不過(guò)還是被春波特提出了兩點(diǎn)建議:
@Configuration public class ListenerContainerConfiguration implements ApplicationContextAware, SmartInitializingSingleton {private ObjectMapper objectMapper = new ObjectMapper(); ~~春波特: 性能上考慮,不要初始化這個(gè)成員變量,既然這個(gè)成員是在構(gòu)造/setter方法里設(shè)置的,就不要在這里初始化,尤其是當(dāng)它的構(gòu)造成本很高的時(shí)候。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); ~~春波特: 下面的這種手工注冊(cè)Bean的方式是Spring 4.x里提供能,可以考慮使用Spring5.0 里提供的 GenericApplicationContext.registerBean的方法,通過(guò)supplier調(diào)用new來(lái)構(gòu)造Bean實(shí)例 [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); ~~春波特: 你這里的啟動(dòng)方法是通過(guò) afterPropertiesSet() 調(diào)用的,這個(gè)是不建議的,應(yīng)該實(shí)現(xiàn)SmartLifecycle來(lái)定義啟停方法,這樣在ApplicationContext刷新時(shí)能夠自動(dòng)啟動(dòng);并且避免了context初始化時(shí)由于底層資源問(wèn)題導(dǎo)致的掛住(stuck)的危險(xiǎn)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)
“還有,還有”,在羅美琪采納了春波特的意見(jiàn)比較大地調(diào)整了代碼之后,春波特哥哥又提出了 Spring Boot 特有的幾個(gè)要求:
- 使用 Spring 的 Assert 在傳統(tǒng)的 Java 代碼中我們使用 assert 進(jìn)行斷言,Spring Boot 中斷言需要使用它自有的 Assert 類,如下示例:
- Auto Configuration 單元測(cè)試使用 Spring 2.0 提供的 ApplicationContextRunner:
- 在 auto-configuration 模塊的 pom.xml 文件里,加入 spring-boot-configuration-processor 注解處理器,這樣它能夠生成輔助元數(shù)據(jù)文件,加快啟動(dòng)時(shí)間。
詳情見(jiàn)這里:
https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-developing-auto-configuration.html#boot-features-custom-starter-module-autoconfigure
最后,春波特還相當(dāng)專業(yè)地向羅美琪美眉提供了如下兩方面的意見(jiàn):
1. 通用的規(guī)范,好的代碼要易讀易于維護(hù)
1)注釋與命名規(guī)范
我們常用的代碼注釋分為多行(/** … */)和單行(// …)兩種類型,對(duì)于需要說(shuō)明的成員變量,方法或者代碼邏輯應(yīng)該提供多行注釋; 有些簡(jiǎn)單的代碼邏輯注釋也可以使用單行注釋。在注釋時(shí)通用的要求是首字母大寫(xiě)開(kāi)頭,并且使用句號(hào)結(jié)尾;對(duì)于單行注釋,也要求首字母大寫(xiě)開(kāi)頭;并且不建議行尾單行注釋。
在變量和方法命名時(shí)盡量用詞準(zhǔn)確,并且盡量不要使用縮寫(xiě),如: sendMsgTimeout,建議寫(xiě)成 sendMessageTimeout;包名 supports,建議改成 support。
2)是否需要使用?Lombok
使用 Lombok 的好處是代碼更加簡(jiǎn)潔,只需要使用一些注釋就可省略 constructor,setter 和 getter 等諸多方法(bolierplate code);但是也有一個(gè)壞處就是需要開(kāi)發(fā)者在自己的 IDE 環(huán)境配置 Lombok 插件來(lái)支持這一功能,所以 Spring 社區(qū)的推薦方式是不使用 Lombok,以便新用戶可以直接查看和維護(hù)代碼,不依賴 IDE 的設(shè)置。
3)對(duì)于包名(package)的控制
如果一個(gè)包目錄下沒(méi)有任何 class,建議要去掉這個(gè)包目錄。例如:org.apache.rocketmq.spring.starter 在 spring 目錄下沒(méi)有具體的 class 定義,那么應(yīng)該去掉這層目錄(編者注: 我們最終把 package 改為 org.apache.rocketmq.spring,將 starter 下的目錄和 classes 上移一層)。我們把所有 Enum 類放在包 org.apache.rocketmq.spring.enums 下,這個(gè)包命名并不規(guī)范,需要把 Enum 類調(diào)整到具體的包中,去掉 enums 包;類的隱藏,對(duì)于有些類,它只被包中的其它類使用,而不需要把具體的使用細(xì)節(jié)暴漏給最終用戶,建議使用 package private 約束,例如:TransactionHandler 類。
4)不建議使用 Static Import, 雖然使用它的好處是更少的代碼,壞處是破壞程序的可讀性和易維護(hù)性。
2. 效率,深入代碼的細(xì)節(jié)
- static + final method:一個(gè)類的 static 方法不要結(jié)合 final,除非這個(gè)類本身是 final 并且聲明 private 構(gòu)造(ctor),如果兩者結(jié)合以為這子類不能再(hiding)定義該方法,給將來(lái)的擴(kuò)展和子類調(diào)用帶來(lái)麻煩。
- 在配置文件聲明的 Bean 盡量使用構(gòu)造函數(shù)或者 Setter 方法設(shè)置成員變量,而不要使用@Autowared,@Resource等方式注入。
- 不要額外初始化無(wú)用的成員變量。
- 如果一個(gè)方法沒(méi)有任何地方調(diào)用,就應(yīng)該刪除;如果一個(gè)接口方法不需要,就不要實(shí)現(xiàn)這個(gè)接口類。
注[4]:下面的截圖是有 FieldInjection 轉(zhuǎn)變成構(gòu)造函數(shù)設(shè)置的代碼示例。
轉(zhuǎn)換成:
故事的結(jié)局
羅美琪根據(jù)上述的要求調(diào)整了代碼,使代碼質(zhì)量有了很大的提高,并且總結(jié)了 Spring Boot 開(kāi)發(fā)的要點(diǎn):
- 編寫(xiě)前參考成熟的 spring boot 實(shí)現(xiàn)代碼。
- 要注意模塊的劃分,區(qū)分 autoconfiguration 和 starter。
- 在編寫(xiě) autoconfiguration Bean 的時(shí)候,注意 @Conditional 注解的使用;盡量使用構(gòu)造器或者 setter 方法來(lái)設(shè)置變量,避免使用 Field Injection 方式;多個(gè) Configuration Bean 可以使用 @Import 關(guān)聯(lián);使用 Spring 2.0 提供的 AutoConfigruation 測(cè)試類。
- 注意一些細(xì)節(jié):static 與 BeanPostProcessor;Lifecycle 的使用;不必要的成員屬性的初始化等。
通過(guò)本次的 Review 工作了解到了 spring-boot 及 auto-configuration 所需要的一些約束條件,信心滿滿地提交了最終的代碼,又可以邀請(qǐng) RocketMQ 社區(qū)的小伙伴們一起使用 rocketmq-spring 功能了,廣大讀者可以在參考代碼庫(kù)查看到最后修復(fù)代碼,也希望有更多的寶貴意見(jiàn)反饋和加強(qiáng),加油!
后記
開(kāi)源軟件不僅僅是提供一個(gè)好用的產(chǎn)品,代碼質(zhì)量和風(fēng)格也會(huì)影響到廣大的開(kāi)發(fā)者,活躍的社區(qū)貢獻(xiàn)者羅美琪還在與 RocketMQ 社區(qū)的小伙伴們不斷完善 spring 的代碼,并邀請(qǐng)春波特的 Spring 社區(qū)進(jìn)行宣講和介紹,下一步將 rocketmq-spring-starter 推進(jìn)到 Spring Initializr,讓用戶可以直接在 start.spring.io 網(wǎng)站上像使用其它 starter(如: Tomcat starter)一樣使用 rocketmq-spring。
釘釘搜索群號(hào):21982288,即可加入 Apache RocketMQ 中國(guó)開(kāi)發(fā)者官方釘釘群!
在 PC 端登錄 start.aliyun.com 知行動(dòng)手實(shí)驗(yàn)室,沉浸式體驗(yàn)在線交互教程。
總結(jié)
以上是生活随笔為你收集整理的罗美琪和春波特的故事...的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 【全球年青人召集令】Hello Worl
- 下一篇: Fluid 给数据弹性一双隐形的翅膀 -