javascript
使用Spring特性优雅书写业务代码
作者:阿里巴巴淘系技術(shù)
鏈接:https://www.zhihu.com/question/60761181/answer/1737592739
來源:知乎
著作權(quán)歸作者所有。商業(yè)轉(zhuǎn)載請聯(lián)系作者獲得授權(quán),非商業(yè)轉(zhuǎn)載請注明出處。
?
分享一套使用Spring特性優(yōu)雅書寫業(yè)務(wù)代碼的方法。
大家在日常業(yè)務(wù)開發(fā)工作中相信多多少少遇到過下面這樣的幾個場景:
- 當(dāng)某一個特定事件或動作發(fā)生以后,需要執(zhí)行很多聯(lián)動動作,如果串行去執(zhí)行的話太耗時,如果引入消息中間件的話又太重了;
- 想要針對不同的傳參執(zhí)行不同的策略,也就是我們常說的策略模式,但10個人可能有10種不同的寫法,夾雜在一起總感覺不那么優(yōu)雅;
- 自己的系統(tǒng)想要調(diào)用其他系統(tǒng)提供的能力,但其他系統(tǒng)總是偶爾給你一點(diǎn)“小驚喜”,可能因網(wǎng)絡(luò)問題報(bào)超時異常或被調(diào)用的某一臺分布式應(yīng)用機(jī)器突然宕機(jī),我們想要優(yōu)雅無侵入式地引入重試機(jī)制。
?
其實(shí)上面提到的幾個典型業(yè)務(wù)開發(fā)場景Spring都為我們提供了很好的特性支持,我們只需要引入Spring相關(guān)依賴就可以方便快速的在業(yè)務(wù)代碼當(dāng)中使用啦,而不用引入過多的三方依賴包或自己重復(fù)造輪子。下面我們就來看看Spring提供的強(qiáng)大魔力吧。
?
使用Spring優(yōu)雅實(shí)現(xiàn)觀察者模式
觀察者模式定義對象間的一種一對多的依賴關(guān)系,當(dāng)一個對象的狀態(tài)發(fā)生改變時,所有依賴于它的對象都得到通知并被自動更新,其主要解決一個對象狀態(tài)改變給其他關(guān)聯(lián)對象通知的問題,保證易用和低耦合。一個典型的應(yīng)用場景是:當(dāng)用戶注冊以后,需要給用戶發(fā)送郵件,發(fā)送優(yōu)惠券等操作,如下圖所示。
?
使用觀察者模式后:
UserService 在完成自身的用戶注冊邏輯之后,僅僅只需要發(fā)布一個 UserRegisterEvent 事件,而無需關(guān)注其它拓展邏輯。其它 Service 可以自己訂閱 UserRegisterEvent 事件,實(shí)現(xiàn)自定義的拓展邏輯。Spring的事件機(jī)制主要由3個部分組成。
- ApplicationEvent:通過繼承它,實(shí)現(xiàn)自定義事件。另外,通過它的 source 屬性可以獲取事件源,timestamp 屬性可以獲得發(fā)生時間。
- ApplicationEventPublisher:通過實(shí)現(xiàn)它,來發(fā)布變更事件。
- ApplicationEventListener:通過實(shí)現(xiàn)它,來監(jiān)聽指定類型事件并響應(yīng)動作。這里就以上面的用戶注冊為例,來看看代碼示例。首先定義用戶注冊事件 UserRegisterEvent。
?
然后定義用戶注冊服務(wù)類,實(shí)現(xiàn) ApplicationEventPublisherAware 接口,從而將 ApplicationEventPublisher 注入進(jìn)來。從下面代碼可以看到,在執(zhí)行完注冊邏輯后,調(diào)用了 ApplicationEventPublisher的 publishEvent(ApplicationEvent event) 方法,發(fā)布了 UserRegisterEvent 事件。
?
@Service publicclass UserService implements ApplicationEventPublisherAware { // <1>private Logger logger = LoggerFactory.getLogger(getClass());private ApplicationEventPublisher applicationEventPublisher;@Overridepublic void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {this.applicationEventPublisher = applicationEventPublisher;}public void register(String username) {// ... 執(zhí)行注冊邏輯logger.info("[register][執(zhí)行用戶({}) 的注冊邏輯]", username);// <2> ... 發(fā)布applicationEventPublisher.publishEvent(new UserRegisterEvent(this, username));} }?
創(chuàng)建郵箱Service,實(shí)現(xiàn) ApplicationListener 接口,通過 E 泛型設(shè)置感興趣的事件,實(shí)現(xiàn) onApplicationEvent(E event) 方法,針對監(jiān)聽的 UserRegisterEvent 事件,進(jìn)行自定義處理。
@Service publicclass EmailService implements ApplicationListener<UserRegisterEvent> { // <1>private Logger logger = LoggerFactory.getLogger(getClass());@Override@Async// <3>public void onApplicationEvent(UserRegisterEvent event) { // <2>logger.info("[onApplicationEvent][給用戶({}) 發(fā)送郵件]", event.getUsername());} }創(chuàng)建優(yōu)惠券Service,不同于上面的實(shí)現(xiàn) ApplicationListener 接口方式,在方法上,添加 @EventListener 注解,并設(shè)置監(jiān)聽的事件為 UserRegisterEvent。這是另一種使用方式。
@Service publicclass CouponService {private Logger logger = LoggerFactory.getLogger(getClass());@EventListener// <1>public void addCoupon(UserRegisterEvent event) {logger.info("[addCoupon][給用戶({}) 發(fā)放優(yōu)惠劵]", event.getUsername());} }看到這里,細(xì)心的同學(xué)可能想到了發(fā)布訂閱模式,其實(shí)觀察者模式于發(fā)布訂閱還是有區(qū)別的,簡單來說,發(fā)布訂閱模式屬于廣義上的觀察者模式,在觀察者模式的 Subject 和 Observer 的基礎(chǔ)上,引入 Event Channel 這個中介,進(jìn)一步解耦。圖示如下,可以看出,觀察者模式更加輕量,通常用于單機(jī),而發(fā)布訂閱模式相對而言更重一些,通常用于分布式環(huán)境下的消息通知場景。
使用Spring Retry優(yōu)雅引入重試機(jī)制
如今,Spring Retry是一個獨(dú)立的包了(早期是Spring Batch的一部分),下面是使用Spring Retry框架進(jìn)行重試的幾個重要步驟。第一步:加入Spring Retry依賴包
<dependency><groupId>org.springframework.retry</groupId><artifactId>spring-retry</artifactId><version>1.1.2.RELEASE</version> </dependency>第二步:在應(yīng)用中包含main()方法的類或者在包含@Configuration的類上加上@EnableRetry注解 第三步:在想要進(jìn)行重試的方法(可能發(fā)生異常)上加上@Retryable注解
@Retryable(maxAttempts=5,backoff = @Backoff(delay = 3000)) public void retrySomething() throws Exception{logger.info("printSomething{} is called");thrownew SQLException(); }在上面這個案例當(dāng)中的重試策略就是重試5次,每次延時3秒。詳細(xì)的使用文檔看這里,它的主要配置參數(shù)有下面這樣幾個。其中exclude、include、maxAttempts、value幾個屬性很容易理解,比較看不懂的是backoff屬性,它也是個注解,包含delay、maxDelay、multiplier、random四個屬性。
- delay:如果不設(shè)置的話默認(rèn)是1秒
- maxDelay:最大重試等待時間
- multiplier:用于計(jì)算下一個延遲時間的乘數(shù)(大于0生效)
- random:隨機(jī)重試等待時間(一般不用)
?
Spring Retry的優(yōu)點(diǎn)很明顯,第一,屬于Spring大生態(tài),使用起來不會太生硬;第二,只需要在需要重試的方法上加上注解并配置重試策略屬性就好,不需要太多侵入代碼。
但同時也存在兩個主要不足,第一,由于Spring Retry用到了Aspect增強(qiáng),所以就會有使用Aspect不可避免的坑——方法內(nèi)部調(diào)用,如果被 @Retryable 注解的方法的調(diào)用方和被調(diào)用方處于同一個類中,那么重試將會失效;第二,Spring的重試機(jī)制只支持對異常進(jìn)行捕獲,而無法對返回值進(jìn)行校驗(yàn)判斷重試。如果想要更靈活的重試策略可以考慮使用Guava Retry,也是一個不錯的選擇。
?
?
優(yōu)雅使用Spring特性完成業(yè)務(wù)策略模式
策略模式相信大家都應(yīng)該比較熟悉,它定義了一系列的算法,并將每一個算法封裝起來,使每個算法可以相互替代,使算法本身和使用算法的客戶端分割開來,相互獨(dú)立。
其適用的場景是這樣的:一個大功能,它有許多不同類型的實(shí)現(xiàn)(策略類),具體根據(jù)客戶端來決定采用哪一個策略類。比如下單優(yōu)惠策略、物流對接策略等,應(yīng)用場景還是非常多的。
舉一個簡單的例子,業(yè)務(wù)背景是這樣的:平臺需要根據(jù)不同的業(yè)務(wù)進(jìn)行鑒權(quán),每個業(yè)務(wù)的鑒權(quán)邏輯不一樣,都有自己的一套獨(dú)立的判斷邏輯,因此需要根據(jù)傳入的 bizType 進(jìn)行鑒權(quán)操作,首先我們定義一個權(quán)限校驗(yàn)處理器接口如下。
/*** 業(yè)務(wù)權(quán)限校驗(yàn)處理器*/ publicinterface PermissionCheckHandler {/*** 判斷是否是自己能夠處理的權(quán)限校驗(yàn)類型*/boolean isMatched(BizType bizType);/*** 權(quán)限校驗(yàn)邏輯*/PermissionCheckResultDTO permissionCheck(Long userId, String bizCode); } 業(yè)務(wù)1的鑒權(quán)邏輯我們假設(shè)是這樣的: /*** 冷啟動權(quán)限校驗(yàn)處理器*/ @Component publicclass ColdStartPermissionCheckHandlerImpl implements PermissionCheckHandler {@Overridepublic boolean isMatched(BizType bizType) {return BizType.COLD_START.equals(bizType);}@Overridepublic PermissionCheckResultDTO permissionCheck(Long userId, String bizCode) {//業(yè)務(wù)特有鑒權(quán)邏輯} } 業(yè)務(wù)2的鑒權(quán)邏輯我們假設(shè)是這樣的: /*** 趨勢業(yè)務(wù)權(quán)限校驗(yàn)處理器*/ @Component publicclass TrendPermissionCheckHandlerImpl implements PermissionCheckHandler {@Overridepublic boolean isMatched(BizType bizType) {return BizType.TREND.equals(bizType);}@Overridepublic PermissionCheckResultDTO permissionCheck(Long userId, String bizCode){//業(yè)務(wù)特有鑒權(quán)邏輯} }可能還有很多其他的業(yè)務(wù)鑒權(quán)邏輯,這里就不一一列舉了,實(shí)現(xiàn)邏輯像上面這樣組織就好了。接著就到了關(guān)鍵的地方了,上面我們定義了這么多策略,應(yīng)該怎么優(yōu)雅的組織起來呢,這就需要用到Spring提供的一些擴(kuò)展特性了,Spring主要為我們提供了三類擴(kuò)展點(diǎn),分別對應(yīng)不同Bean生命周期階段:
- Aware接口
- BeanPostProcessor
- InitializingBean 和 init-method
我們這里用到的主要是 Aware 接口和 InitializingBean 兩個擴(kuò)展點(diǎn),其主要用法如下代碼所示,關(guān)鍵點(diǎn)就在于實(shí)現(xiàn) ApplicationContextAware 接口的 setApplicationContext 方法和 InitializingBean 接口的 afterPropertiesSet 方法。
實(shí)現(xiàn) ApplicationContextAware 接口的目的就是要拿到 Spring 容器的資源,從而方便的使用它提供的 getBeansOfType 方法(該方法返回的是 map 類型,key 對應(yīng) beanName, value 對應(yīng) bean);而實(shí)現(xiàn) InitializingBean 接口的目的則是方便為 Service 類的 handlers 屬性執(zhí)行定制初始化邏輯。
?
可以很明顯的看出,如果以后還有一些其他的業(yè)務(wù)需要制定相應(yīng)的鑒權(quán)邏輯,我們只需要編寫對應(yīng)的策略類就好了,無需再破壞當(dāng)前 Service 類的邏輯,很好的保證了開閉原則。
/*** 權(quán)限校驗(yàn)服務(wù)類*/ @Slf4j @Service publicclass PermissionServiceImplimplements PermissionService, ApplicationContextAware, InitializingBean {private ApplicationContext applicationContext;//注:這里可以使用Map,偷個懶private List<PermissionCheckHandler> handlers = new ArrayList<>();@Overridepublic PermissionCheckResultDTO permissionCheck(ArtemisSellerBizType artemisSellerBizType, Long userId,String bizCode) {//省略一些前置邏輯PermissionCheckHandler handler = getHandler(artemisSellerBizType);return handler.permissionCheck(userId, bizCode);}private PermissionCheckHandler getHandler(ArtemisSellerBizType artemisSellerBizType) {for (PermissionCheckHandler handler : handlers) {if (handler.isMatched(artemisSellerBizType)) {return handler;}}returnnull;}@Overridepublic void afterPropertiesSet() throws Exception {for (PermissionCheckHandler handler : applicationContext.getBeansOfType(PermissionCheckHandler.class).values()) {handlers.add(handler);log.warn("load permission check handler [{}]", handler.getClass().getName());}}@Overridepublic void setApplicationContext(ApplicationContext applicationContext) throws BeansException {this.applicationContext = applicationContext;} }當(dāng)然在這里相信不少同學(xué)會有疑問,那就是這里在獲取 handler 處理器 bean 的時候,所有的 bean 是不是已經(jīng)初始化好了?會不會存在有的 handler 還沒有初始化好的情況?
?
答案是不會的,Spring Bean 的聲明周期保證了這一點(diǎn)(當(dāng)然前提是 handler 自身不會有特殊的初始化邏輯)。經(jīng)過實(shí)際驗(yàn)證,所有的 handler 會在 Service 初始化操作前 ready,感興趣的同學(xué)可以編寫代碼驗(yàn)證,可以先在相應(yīng)鉤子處打上日志直接輸出結(jié)果驗(yàn)證,然后在 Spring 源碼關(guān)鍵處打上斷點(diǎn) debug,相信會有不少收獲。
?
總結(jié)&思考
公司里的有些代碼有點(diǎn)年齡,有些類寫的又臭又長,很多地方充斥著代碼壞味道,如重復(fù)的代碼,過長的參數(shù)列,散彈式修改,基本型偏執(zhí)等等,不一一展開。每天要面對這些代碼進(jìn)行開發(fā),不僅消磨了我們對技術(shù)的熱情也讓人變得毫無斗志,很多同學(xué)會想——反正都已經(jīng)這樣了,那我也就這么來吧,相信不少小伙伴都有這樣的遭遇與困惑。
但唯一不能停下來的就是進(jìn)步,即使面對惡龍還是不能放棄抵抗。當(dāng)然,在做需求的時候,很多時候也不能去修改那些代碼,太耗時太費(fèi)勁,風(fēng)險(xiǎn)太大。那自己起碼也要思考一下如何設(shè)計(jì)代碼才能去避免以后出現(xiàn)同樣的情況,讓自己下次不要犯同樣的錯誤。
當(dāng)我們在實(shí)際編寫代碼的時候,需要留意探索一下Spring有沒有為我們提供一些已有的工具類和擴(kuò)展點(diǎn)。一方面,使用Spring提供的這些特性可以讓我們少造輪子,避免引入其他比較重的類庫;另一方面,Spring對JDK等庫提供的一些類和規(guī)范進(jìn)行了抽象封裝,易用性更好,更貼合開發(fā)者需求。
總結(jié)
以上是生活随笔為你收集整理的使用Spring特性优雅书写业务代码的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: @Transactional 注解的失效
- 下一篇: 基于 Kafka 技术栈构建和部署实时搜