本文欲回答這樣一個(gè)問(wèn)題:在 「特定環(huán)境 」下卧秘,如何規(guī)劃Web開(kāi)發(fā)框架,使其能滿(mǎn)足 「期望 」包晰?
假設(shè)我們的「特定環(huán)境 」如下:
- 技術(shù)層面
- 使用Java語(yǔ)言進(jìn)行開(kāi)發(fā)
- 通過(guò)Maven構(gòu)建
- 基于SpringBoot
- 使用IntellijIDEA作為IDE
- 使用Mybatis作為持久層框架
- 前后端分離
- 非技術(shù)層面
- 新項(xiàng)目顷歌,變化較頻繁
- 快速迭代
- 開(kāi)發(fā)人員資歷較淺
- 人員流動(dòng)性較大
我們的 「期望 」是:
- 快速上手:鑒于人員流動(dòng)性較大、開(kāi)發(fā)人員的資歷較淺和項(xiàng)目的快速迭代需求锣险,期望開(kāi)發(fā)框架易于開(kāi)發(fā)人員開(kāi)發(fā)。易于入門(mén)览闰,易于部署芯肤。
- 符合行業(yè)規(guī)約:盡量不定義私有規(guī)范,使用行業(yè)標(biāo)準(zhǔn)压鉴,進(jìn)一步降低學(xué)習(xí)難度
- 快速開(kāi)發(fā):盡可能復(fù)用代碼崖咨,盡可能自動(dòng)化生成模板代碼
- 獨(dú)立性:應(yīng)用能獨(dú)立運(yùn)行,不過(guò)多的依賴(lài)其它應(yīng)用或中間件晴弃。邊界清晰掩幢,有利于理解、開(kāi)發(fā)上鞠、測(cè)試和部署际邻。反例:就是沒(méi)有規(guī)劃的RPC調(diào)用。
- 易于測(cè)試:能方便的進(jìn)行單元/集成測(cè)試芍阎,不影響真實(shí)數(shù)據(jù)
- 易于部署:能方便的進(jìn)行部署世曾,便于快速的擴(kuò)容
- 異常可追蹤:對(duì)異常谴咸,可快速定位到具體是哪個(gè)應(yīng)用轮听,哪個(gè)類(lèi),哪行代碼的問(wèn)題
本文從一個(gè)空框架開(kāi)始岭佳,逐步加入上面的約束血巍,最終推導(dǎo)出符合期望的Web框架!
本文提供的是一種思路珊随!如有紕漏述寡、或不同意見(jiàn),歡迎討論指正叶洞!
從「空框架」開(kāi)始
我們從一個(gè)「空框架」開(kāi)始我們的框架推導(dǎo)鲫凶!所謂「空框架」是一個(gè)沒(méi)有任何約束的接收HTTP的可運(yùn)行代碼,比如對(duì)任何請(qǐng)求都只返回Hello World的servlet衩辟!
這里我們基于Maven和SpringBoot快速搭建一個(gè)「空框架」螟炫!
代碼結(jié)構(gòu)如下(Maven構(gòu)建約束):
intellijweb2
src/main
java
com.ivaneye.intellijweb2
TestController
resources
application.properties
logback-spring.xml
代碼如下:
package com.ivaneye.intellijweb2;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
@EnableAutoConfiguration
public class TestController {
@RequestMapping("/")
@ResponseBody
public String home() {
return "Hello World!";
}
public static void main(String[] args) throws Exception {
SpringApplication.run(Main.class, args);
}
}
啟動(dòng)后,當(dāng)訪(fǎng)問(wèn)http://localhost:8080時(shí)艺晴,頁(yè)面上將顯示Hello world!字樣昼钻!
我們完全可以基于這個(gè)「空框架」進(jìn)行開(kāi)發(fā)掸屡,但是這個(gè)「空框架」離我們的期望還很遠(yuǎn)。我們來(lái)一步步的改造然评!
分層架構(gòu)
分層架構(gòu)可以說(shuō)是Web項(xiàng)目的默認(rèn)架構(gòu)風(fēng)格折晦,可以說(shuō)是行業(yè)標(biāo)準(zhǔn)!所以我們首先引入分層架構(gòu)這個(gè)約束沾瓦!
分層架構(gòu)有其優(yōu)勢(shì)和劣勢(shì):
優(yōu)勢(shì):通過(guò)將組件對(duì)系統(tǒng)的知識(shí)限制在單一層內(nèi),為整個(gè)系統(tǒng)的復(fù)雜性設(shè)置了邊界谦炒,并且提高了底層獨(dú)立性贯莺。使用層來(lái)封裝遺留的服務(wù),使新的服務(wù)免受遺留客戶(hù)端的影響宁改;通過(guò)將不常用的功能轉(zhuǎn)移到一個(gè)共享的中間組件中缕探,從而簡(jiǎn)化組件的實(shí)現(xiàn)。中間組件還能夠通過(guò)支持跨多個(gè)網(wǎng)絡(luò)和處理器的負(fù)載均衡还蹲,來(lái)改善系統(tǒng)的可伸縮性爹耗。
劣勢(shì):增加了數(shù)據(jù)處理的開(kāi)銷(xiāo)和延遲,因此降低了用戶(hù)可覺(jué)察的性能谜喊√妒蓿可以通過(guò)在中間層使用共享緩存來(lái)彌補(bǔ)這一缺點(diǎn)。
Web里最常用的切分方式就是MVC模式斗遏!我們對(duì)我們的「空框架」引入MVC模式山卦!
那我們這里是切分包?還是切分模塊呢诵次?考慮到最小影響原則账蓉,這里先切分包。如果有后續(xù)約束逾一,再做進(jìn)一步調(diào)整铸本。
引入MVC模式后的代碼結(jié)構(gòu):
intellijweb2
src/main
java
com.ivaneye.intellijweb2
controller
TestController
model
respository
service
Main
resources
application.properties
logback-spring.xml
引入MVC模式后的代碼:
package com.ivaneye.intellijweb2;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
@EnableAutoConfiguration
@ComponentScan({"com.ivaneye.intellijweb2"})
public class Main {
public static void main(String[] args) throws Exception {
SpringApplication.run(Main.class, args);
}
}
package com.ivaneye.intellijweb2.controller;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
public class TestController {
@RequestMapping("/")
@ResponseBody
public String home() {
return "Hello World!";
}
}
這里暫時(shí)切分了Controller,Service,Model,Respository四個(gè)包,職責(zé)如下:
Controller:接收前臺(tái)的請(qǐng)求遵堵,驗(yàn)證數(shù)據(jù)箱玷,組裝需要的數(shù)據(jù),委托Service執(zhí)行具體業(yè)務(wù)邏輯鄙早,并將結(jié)果組裝返回給前臺(tái)
Service:處理核心業(yè)務(wù)邏輯汪茧,包含事務(wù)
Model:數(shù)據(jù)模型,與數(shù)據(jù)庫(kù)表的對(duì)應(yīng)類(lèi)
Respository:數(shù)據(jù)操作類(lèi)包限番,操作Model中的類(lèi)舱污,進(jìn)行基本的CRUD操作
分層后的框架邏輯清晰,且切分方式符合行業(yè)規(guī)約弥虐,更易于上手扩灯。
前后端分離
考慮到媚赖,目前Web開(kāi)發(fā)流行前后端分離,為了適應(yīng)潮流珠插,引入前后端分離的約束惧磺。
為了適應(yīng)前后端分離,后端不負(fù)責(zé)頁(yè)面的渲染捻撑,只接收和返回JSON數(shù)據(jù)磨隘。SpringBoot對(duì)此有直接的支持,直接將@Controller改為@RestController即可顾患!
相關(guān)代碼:
package com.ivaneye.intellijweb2.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class TestController {
@RequestMapping("/")
public String home() {
return "Hello World!";
}
}
整個(gè)URL符合RESTful番捂,即符合行業(yè)規(guī)約!至于REST相關(guān)內(nèi)容另行討論江解。
實(shí)際上完整的RESTful應(yīng)用不只是URL符合RESTful设预,需要符合四個(gè)核心的約束:
資源的識(shí)別(identification of resources)
通過(guò)表述操作資源(manipulation of resources through representations)
自描述的消息(self-descriptive messages)
超媒體作為應(yīng)用狀態(tài)引擎(hypermedia as the engine of application state)
絕大部分聲稱(chēng)符合RESTful的應(yīng)用都不是百分百符合這四個(gè)約束,特別是超媒體作為應(yīng)用狀態(tài)引擎(hypermedia as the engine of application state)這個(gè)約束犁河。
基于注解的數(shù)據(jù)處理
確定了以JSON的方式進(jìn)行參數(shù)的傳遞后鳖枕,就需要確定如何來(lái)處理參數(shù)和返回結(jié)果?這涉及到幾個(gè)問(wèn)題:
Controller如何接收參數(shù)桨螺?
Controller如何返回結(jié)果宾符?
Controller如何將數(shù)據(jù)傳遞給Respository進(jìn)行持久化處理?
Respository又如何將數(shù)據(jù)從數(shù)據(jù)庫(kù)中查出來(lái)返回給Controller?
這里選擇了Mybatis作為持久化框架彭谁,我們先從Mybatis的角度來(lái)回答上面的幾個(gè)問(wèn)題吸奴!
首先Mybatis作為框架,會(huì)生成幾個(gè)文件:Model.java,Mapper.java和Mapper.xml2帧(這里不做過(guò)多解釋?zhuān)?duì)Mybatis不熟悉的朋友請(qǐng)自行g(shù)oogleT虬隆)這幾個(gè)文件可以自動(dòng)生成,也可以手寫(xiě)狭园!
不論是自動(dòng)生成還是手寫(xiě)都有其優(yōu)缺點(diǎn):
-
先說(shuō)自動(dòng)生成的優(yōu)缺點(diǎn):
優(yōu)點(diǎn)就是在修改表結(jié)構(gòu)以后读处,直接一條命令就可以自動(dòng)生成新文件。
缺點(diǎn)就是這三個(gè)文件不能修改唱矛,如果修改了就不能再次自動(dòng)生成了罚舱,否則會(huì)被覆蓋。
-
手動(dòng)編寫(xiě)的優(yōu)缺點(diǎn):
優(yōu)點(diǎn)是完全自主控制绎谦,可復(fù)用Model管闷,在里面添加注解,實(shí)現(xiàn)數(shù)據(jù)驗(yàn)證窃肠、主鍵加解密包个、字典自動(dòng)查詢(xún)等邏輯。
缺點(diǎn)就是表結(jié)構(gòu)調(diào)整后冤留,需要手動(dòng)修改需要調(diào)整的文件碧囊。一是繁瑣树灶,二是沒(méi)有編譯期校驗(yàn),如果手誤寫(xiě)錯(cuò)了糯而,直到運(yùn)行期才可能發(fā)現(xiàn)
一種優(yōu)化方案是天通,第一次使用自動(dòng)生成,后續(xù)手動(dòng)修改熄驼。
但是結(jié)合前面的約束:
- 新項(xiàng)目像寒,變化較頻繁
- 快速迭代
- 開(kāi)發(fā)人員資歷較淺
此方法并不適用。 此方法只對(duì)于改動(dòng)不太頻繁的項(xiàng)目還算適用瓜贾,但是如果表結(jié)構(gòu)改動(dòng)較頻繁萝映,后續(xù)的每次修改還是要手動(dòng)修改,非常的麻煩(無(wú)法適應(yīng)頻繁的變更阐虚,快速迭代)。且只能第一次使用自動(dòng)生成這個(gè)規(guī)定并沒(méi)法強(qiáng)制實(shí)施蚌卤,你沒(méi)法保證誰(shuí)不會(huì)誤操作了自動(dòng)生成(考慮開(kāi)發(fā)人員資歷較淺)实束,導(dǎo)致手寫(xiě)的代碼被覆蓋了!
結(jié)合以上約束逊彭,為了盡量避免錯(cuò)誤咸灿,優(yōu)先選擇自動(dòng)生成!再來(lái)嘗試解決其短板侮叮,即生成的三個(gè)文件無(wú)法進(jìn)行修改避矢。是否有可行方案呢?
我們先考慮幾個(gè)問(wèn)題:
Controller需要對(duì)頁(yè)面?zhèn)鬟^(guò)來(lái)的參數(shù)做哪些操作囊榜?
頁(yè)面?zhèn)鱽?lái)的參數(shù)和Model是一個(gè)什么關(guān)系审胸?
從Controller返回給頁(yè)面的數(shù)據(jù)又和Model是什么關(guān)系?
Controller對(duì)返回給頁(yè)面的數(shù)據(jù)又要做哪些操作卸勺?
為方便起見(jiàn)砂沛,我們把入?yún)⒎Q(chēng)為Param,返回結(jié)果稱(chēng)為Result曙求。我們先回答第一個(gè)和第四個(gè)問(wèn)題碍庵!
-
Controller需要對(duì)Param做哪些操作?
把從頁(yè)面?zhèn)鬟f過(guò)來(lái)的flat數(shù)據(jù)transform為對(duì)象(這是面向?qū)ο笳Z(yǔ)言的一種典型做法悟狱,我目前更偏向函數(shù)式做法静浴,另開(kāi)一篇討論)
對(duì)數(shù)據(jù)做校驗(yàn):類(lèi)型對(duì)不對(duì)、格式對(duì)不對(duì)挤渐、是否為空等等等等
解密:有些字段數(shù)據(jù)可能是加過(guò)密的苹享,比如主鍵,在transform的過(guò)程中需要對(duì)這些字段進(jìn)行解密處理
-
Controller需要對(duì)Result做哪些操作挣菲?
加密:對(duì)需要加密的字段進(jìn)行加密操作富稻,比如主鍵
字典轉(zhuǎn)換:有些字段是code碼掷邦,頁(yè)面需要code碼對(duì)應(yīng)的值,方便人類(lèi)閱讀椭赋。這里需要根據(jù)這些code碼從字典中獲取對(duì)應(yīng)的值(你可以在數(shù)據(jù)庫(kù)查詢(xún)的時(shí)候抚岗,直接關(guān)聯(lián)字典表查詢(xún),但是這樣會(huì)帶來(lái)兩個(gè)麻煩哪怔,一個(gè)是model中需要包含字典value字段宣蔚,就沒(méi)法自動(dòng)生成了。第二個(gè)就是认境,一般字典會(huì)放在內(nèi)存中胚委,關(guān)聯(lián)表查詢(xún)相對(duì)內(nèi)存取數(shù)據(jù),性能上會(huì)有劣勢(shì))
字典列表:和字典轉(zhuǎn)換類(lèi)似叉信,有些頁(yè)面需要字典列表數(shù)據(jù)亩冬,需要獲取這些數(shù)據(jù)到前臺(tái)供用戶(hù)選擇
這些操作都可以方便的處理:
SpringMVC已經(jīng)提供了數(shù)據(jù)綁定功能,將數(shù)據(jù)綁定到對(duì)象上
JSR303基于注解進(jìn)行校驗(yàn)
加解密硼身、字典都可以通過(guò)自定義注解處理(擴(kuò)展Jackson的注解處理即可硅急。Jackson的注解只在方法上生效,本以為是個(gè)問(wèn)題佳遂,卻助我構(gòu)思了一個(gè)方案:一個(gè)結(jié)合了自動(dòng)生成的方便性和手寫(xiě)的靈活性的方案S唷!3笞铩荚板!)
這些都是規(guī)約!
針對(duì)第二個(gè)和第三個(gè)問(wèn)題吩屹,我們先看Param跪另、Result和Model之間的關(guān)系:
從上圖可以看出,除了第一種情況(且這種情況很少)煤搜,其它四種情況Param和Model實(shí)際是一個(gè)包含的關(guān)系狸棍。既然是一種包含的情況妓蛮,那這種包含關(guān)系府喳,在Java里我們可以使用繼承來(lái)實(shí)現(xiàn)窗声。也就是說(shuō)可以使Param extends Model,以這樣的方式來(lái)復(fù)用Model的內(nèi)容厌衙!
我們來(lái)看以這種方式來(lái)實(shí)現(xiàn)Param和Result距淫,如何來(lái)解決上面的問(wèn)題!
首先婶希,因?yàn)镻aram和Result都繼承了Model榕暇,所以Model是不需要做任何改動(dòng)的,就可以無(wú)限次的自動(dòng)生成
其次,數(shù)據(jù)驗(yàn)證彤枢、加解密的注解是可以添加到方法上的狰晚。我們對(duì)需要這些注解的字段,在Param/Result里覆蓋Model里的get/set方法缴啡,在其上添加注解壁晒,就可以使用基于注解的數(shù)據(jù)驗(yàn)證和加解密。
假設(shè)數(shù)據(jù)字段有了修改业栅,重新生成后秒咐,由于有@Override注解,在編譯期就可以定位到需要修改的get/set方法碘裕,結(jié)合IDE可以快速修復(fù)
如果是新增字段携取,則直接重新生成Mybatis的三個(gè)文件即可,原有代碼不受任何影響
盡量以擴(kuò)展規(guī)約的方式來(lái)處理問(wèn)題帮孔,在不增加理解難度的情況下提高易用性和開(kāi)發(fā)效率雷滋!
數(shù)據(jù)返回
在RESTful約束中,推薦使用HTTP的標(biāo)準(zhǔn)響應(yīng)來(lái)處理返回?cái)?shù)據(jù)文兢。SpringMVC中也提供了標(biāo)準(zhǔn)響應(yīng)的支持惊豺。
ResponseEntity.ok("body");
ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("");
但是由于HTTP的標(biāo)準(zhǔn)狀態(tài)碼太少了,見(jiàn)下表:
代碼 | 消息 | 描述 |
---|---|---|
100 | Continue | 只有請(qǐng)求的一部分已經(jīng)被服務(wù)器接收,但只要它沒(méi)有被拒絕禽作,客戶(hù)端應(yīng)繼續(xù)該請(qǐng)求。 |
101 | Switching Protocols | 服務(wù)器切換協(xié)議揩页。 |
200 | OK | 請(qǐng)求成功旷偿。 |
201 | Created | 該請(qǐng)求是完整的,并創(chuàng)建一個(gè)新的資源爆侣。 |
202 | Accepted | 該請(qǐng)求被接受處理萍程,但是該處理是不完整的。 |
203 | Non-authoritative Information | |
204 | No Content | |
205 | Reset Content | |
206 | Partial Content | |
300 | Multiple Choices | 鏈接列表兔仰。用戶(hù)可以選擇一個(gè)鏈接茫负,進(jìn)入到該位置。最多五個(gè)地址 |
301 | Moved Permanently | 所請(qǐng)求的頁(yè)面已經(jīng)轉(zhuǎn)移到一個(gè)新的 URL乎赴。 |
302 | Found | 所請(qǐng)求的頁(yè)面已經(jīng)臨時(shí)轉(zhuǎn)移到一個(gè)新的 URL忍法。 |
303 | See Other | 所請(qǐng)求的頁(yè)面可以在另一個(gè)不同的 URL 下被找到。 |
304 | Not Modified | |
305 | Use Proxy | |
306 | Unused | 在以前的版本中使用該代碼¢藕穑現(xiàn)在已不再使用它饿序,但代碼仍被保留。 |
307 | Temporary Redirect | 所請(qǐng)求的頁(yè)面已經(jīng)臨時(shí)轉(zhuǎn)移到一個(gè)新的 URL羹蚣。 |
400 | Bad Request | 服務(wù)器不理解請(qǐng)求原探。 |
401 | Unauthorized | 所請(qǐng)求的頁(yè)面需要用戶(hù)名和密碼。 |
402 | Payment Required | 你還不能使用該代碼。 |
403 | Forbidden | 禁止訪(fǎng)問(wèn)所請(qǐng)求的頁(yè)面咽弦。 |
404 | Not Found | 服務(wù)器無(wú)法找到所請(qǐng)求的頁(yè)面徒蟆。 |
405 | Method Not Allowed | 在請(qǐng)求中指定的方法是不允許的。 |
406 | Not Acceptable | 服務(wù)器只生成一個(gè)不被客戶(hù)端接受的響應(yīng)型型。 |
407 | Proxy Authentication Required | 在請(qǐng)求送達(dá)之前段审,您必須使用代理服務(wù)器的驗(yàn)證。 |
408 | Request Timeout | 請(qǐng)求需要的時(shí)間比服務(wù)器能夠等待的時(shí)間長(zhǎng)输莺,超時(shí)戚哎。 |
409 | Conflict | 請(qǐng)求因?yàn)闆_突無(wú)法完成。 |
410 | Gone | 所請(qǐng)求的頁(yè)面不再可用嫂用。 |
411 | Length Required | "Content-Length" 未定義型凳。服務(wù)器無(wú)法處理客戶(hù)端發(fā)送的不帶 Content-Length 的請(qǐng)求信息。 |
412 | Precondition Failed | 請(qǐng)求中給出的先決條件被服務(wù)器評(píng)估為 false嘱函。 |
413 | Request Entity Too Large | 服務(wù)器不接受該請(qǐng)求甘畅,因?yàn)檎?qǐng)求實(shí)體過(guò)大。 |
414 | Request-url Too Long | 服務(wù)器不接受該請(qǐng)求往弓,因?yàn)?URL 太長(zhǎng)疏唾。當(dāng)你轉(zhuǎn)換一個(gè) “post” 請(qǐng)求為一個(gè)帶有長(zhǎng)的查詢(xún)信息的 “get” 請(qǐng)求時(shí)發(fā)生。 |
415 | Unsupported Media Type | 服務(wù)器不接受該請(qǐng)求函似,因?yàn)槊襟w類(lèi)型不被支持槐脏。 |
417 | Expectation Failed | |
500 | Internal Server Error | 未完成的請(qǐng)求。服務(wù)器遇到了一個(gè)意外的情況撇寞。 |
501 | Not Implemented | 未完成的請(qǐng)求顿天。服務(wù)器不支持所需的功能。 |
502 | Bad Gateway | 未完成的請(qǐng)求蔑担。服務(wù)器從上游服務(wù)器收到無(wú)效響應(yīng)牌废。 |
503 | Service Unavailable | 未完成的請(qǐng)求。服務(wù)器暫時(shí)超載或死機(jī)啤握。 |
504 | Gateway Timeout | 網(wǎng)關(guān)超時(shí)鸟缕。 |
505 | HTTP Version Not Supported | 服務(wù)器不支持“HTTP協(xié)議”版本。 |
這些標(biāo)準(zhǔn)的狀態(tài)碼無(wú)法詳細(xì)的表示一個(gè)項(xiàng)目中的所有情況排抬。且目前SpringMVC不支持自定義狀態(tài)碼懂从。就是類(lèi)似這樣的代碼:
ResponseEntity.status(10001).body("");
雖然不報(bào)錯(cuò),但是無(wú)法正常響應(yīng)蹲蒲,后臺(tái)會(huì)報(bào)類(lèi)似“非標(biāo)準(zhǔn)狀態(tài)碼”的錯(cuò)誤莫绣!
所以我自定義了一個(gè)對(duì)象Result,用來(lái)完成類(lèi)似ResponseEntity的工作悠鞍。Result的結(jié)構(gòu)如下:
public class Result {
private int code;//200為正常对室,其它為相關(guān)業(yè)務(wù)報(bào)錯(cuò)
private String msg;//對(duì)應(yīng)的錯(cuò)誤信息,200為ok
private Object body;//返回的業(yè)務(wù)對(duì)象
}
提供類(lèi)似:
Result.ok("body")
Result.error(e);
Result.error(CommonConstants.SERVER_ERROR, e.getMessage());
這樣的構(gòu)造方法模燥,方便使用。
異常處理
異常處理在上面數(shù)據(jù)返回里涉及了一點(diǎn)(就是Result的構(gòu)造以及業(yè)務(wù)的各種場(chǎng)景處理)掩宜。這里詳細(xì)說(shuō)明蔫骂。
約束中需要能方便的追蹤異常!
Java里提供了CheckedException和UnCheckedException牺汤,而對(duì)于我們實(shí)際使用來(lái)說(shuō)辽旋,還是需要區(qū)分業(yè)務(wù)場(chǎng)景。
- 異常是業(yè)務(wù)異常還是非業(yè)務(wù)異常檐迟?
這里的業(yè)務(wù)異常指的是:由于不符合業(yè)務(wù)需求而導(dǎo)致的異常补胚,比如:用戶(hù)沒(méi)登錄,必要字段沒(méi)填寫(xiě)導(dǎo)致校驗(yàn)失敗追迟,訂單的數(shù)量超出了庫(kù)存溶其。
非業(yè)務(wù)異常則指的是:和業(yè)務(wù)場(chǎng)景不相關(guān)的異常。例如:數(shù)據(jù)庫(kù)連接失敗了敦间,網(wǎng)絡(luò)連接失敗瓶逃。
表現(xiàn)到代碼上,對(duì)于業(yè)務(wù)異常我們可以定義BusinessException來(lái)表示廓块,所有繼承了BusinessException的異常厢绝,都是業(yè)務(wù)異常,而其它異常就是非業(yè)務(wù)異常带猴。
- 更進(jìn)一步昔汉,業(yè)務(wù)異常也可以分為:
通用業(yè)務(wù)異常,例如:用戶(hù)沒(méi)有登錄拴清,必要字段沒(méi)填寫(xiě)導(dǎo)致校驗(yàn)失敯胁 ;
和特定業(yè)務(wù)異常贷掖,例如:訂單的數(shù)量超出庫(kù)存了。
這兩種異常渴语,我們可以通過(guò)異常碼來(lái)區(qū)分苹威,例如:100開(kāi)頭的為通用業(yè)務(wù)異常,300開(kāi)頭的為訂單異常驾凶,400開(kāi)頭的為產(chǎn)品異常牙甫,依此類(lèi)推。
同時(shí)異常的Code和Msg與Result對(duì)應(yīng)调违,方便構(gòu)建Result.error(e);直接返回窟哺。
再進(jìn)一步,目前的應(yīng)用都是分布式的技肩,甚至是微服務(wù)架構(gòu)且轨!我們是否可以通過(guò)異常能快速的定位到是哪個(gè)應(yīng)用的哪個(gè)模塊里的哪個(gè)代碼出問(wèn)題了呢?
一種可行方案還是通過(guò)異常碼來(lái)處理:以三位數(shù)字為間隔,來(lái)區(qū)分應(yīng)用+模塊+代碼旋奢,例如:001002301泳挥,可以理解為異常是001機(jī)器上的,002應(yīng)用至朗,拋出的301(訂單相關(guān))異常屉符。
獨(dú)立性
當(dāng)系統(tǒng)變得越來(lái)越大后,難免不會(huì)出現(xiàn)系統(tǒng)內(nèi)不同應(yīng)用之間的相互調(diào)用锹引;如果是微服務(wù)的話(huà)矗钟,那么服務(wù)間的相互調(diào)用是很常見(jiàn)的。如果處理不當(dāng)嫌变,會(huì)使得各應(yīng)用之間相互依賴(lài)吨艇,無(wú)法獨(dú)立的運(yùn)行。導(dǎo)致開(kāi)發(fā)初澎、測(cè)試秸应、部署都很麻煩。
為了避免這樣的問(wèn)題出現(xiàn)碑宴,結(jié)合如下兩個(gè)約束:
符合行業(yè)規(guī)約
獨(dú)立性
故使用RESTful方式软啼,作為應(yīng)用間通信的方式。這也是微服務(wù)推薦的通信方式延柠!
應(yīng)用間調(diào)用會(huì)出現(xiàn)Model的依賴(lài)祸挪,故這里將Model從包提升為模塊。方便后續(xù)如果有其它應(yīng)用要依賴(lài)時(shí)贞间,可直接依賴(lài)Model模塊贿条,而不是整個(gè)應(yīng)用。
調(diào)整后代碼結(jié)構(gòu)如下:
intellijweb2
intellijweb2-web
src/main
java
com.ivaneye.intellijweb2
controller
TestController
respository
service
Main
resources
application.properties
logback-spring.xml
intellijweb2-model
src/main
java
com.ivaneye.intellijweb2
model
param
result
將model包移動(dòng)到了intellijweb2-model模塊中增热,同時(shí)新增了param和result包整以!
測(cè)試
SpringBoot本身提供了較為完善的測(cè)試功能。包括單元測(cè)試峻仇、Mocker公黑、Spy等。
基于如下幾個(gè)考慮:
- 易于測(cè)試:我接觸的很多開(kāi)發(fā)人員是不喜歡寫(xiě)測(cè)試的摄咆。如果測(cè)試代碼不易編寫(xiě)凡蚜,那就更不愿意寫(xiě)了。
- 不影響環(huán)境:我期望的是在發(fā)布時(shí)是包含測(cè)試的吭从,測(cè)試不通過(guò)即不能發(fā)布朝蜘。也就是說(shuō)在部署時(shí)測(cè)試,會(huì)使用正式環(huán)境的庫(kù)表數(shù)據(jù)涩金,所以在測(cè)試時(shí)不能影響到這些數(shù)據(jù)谱醇。
- 小范圍測(cè)試:以最少的代碼暇仲,覆蓋最核心的代碼邏輯
故決定只對(duì)Service測(cè)試,原因如下:
- 在上面的分層架構(gòu)里描述了各層的職責(zé)枣抱,可以看出熔吗,核心業(yè)務(wù)都在Service層,Controller和Model都沒(méi)有業(yè)務(wù)邏輯佳晶,只是一些標(biāo)準(zhǔn)化代碼桅狠,沒(méi)必要測(cè)試
- SpringBoot對(duì)Controller的測(cè)試是在不同的線(xiàn)程內(nèi),不支持事務(wù)轿秧,如果在正式環(huán)境測(cè)試的話(huà)中跌,會(huì)影響正式庫(kù)數(shù)據(jù)
部署
SpringBoot可以直接打包為jar包,直接運(yùn)行啟動(dòng)菇篡。這很方便漩符,但是如果想快速的橫向擴(kuò)容,配置文件就是一個(gè)問(wèn)題驱还。因?yàn)椴煌瑱C(jī)器上的配置并不是完全相同的嗜暴。
有兩個(gè)方案可以解決:
Docker
配置服務(wù)器
從便利性考慮,還是選擇配置服務(wù)器议蟆。
配置文件中均是開(kāi)發(fā)環(huán)境配置闷沥,方便開(kāi)發(fā)人員直接開(kāi)發(fā)、測(cè)試咐容。
在正式環(huán)境中舆逃,應(yīng)用啟動(dòng)時(shí)會(huì)從配置服務(wù)器獲取對(duì)應(yīng)的配置,覆蓋本地測(cè)試進(jìn)行部署戳粒。
代碼生成OR封裝
在結(jié)束之前路狮,先問(wèn)個(gè)問(wèn)題?你是喜歡代碼生成蔚约、還是封裝奄妨?
代碼生成就類(lèi)似Mybatis這樣生成了對(duì)應(yīng)的文件,邏輯透明苹祟。你可以去改
封裝就類(lèi)似Hibernate砸抛,你寫(xiě)個(gè)對(duì)象,然后對(duì)對(duì)象操作就行了苔咪,底層數(shù)據(jù)庫(kù)操作由Hibernate來(lái)處理
我個(gè)人更偏向代碼生成锰悼,理由是:
簡(jiǎn)單:易于使用柳骄,易于上手
行業(yè)標(biāo)準(zhǔn):生成的代碼是行業(yè)標(biāo)準(zhǔn)代碼团赏,只要熟悉Mybatis,Spring就可以直接上手(而Mybatis和Spring目前是互聯(lián)網(wǎng)標(biāo)配)。如果公司內(nèi)部進(jìn)行一些封裝耐薯,那么新手需要先理解這些封裝舔清,增加了學(xué)習(xí)成本丝里。
基于上面的原因,再考慮到其實(shí)我們的框架都是符合規(guī)約的(RESTful体谒,JSR303杯聚,覆寫(xiě),Jackson)抒痒,故對(duì)于標(biāo)準(zhǔn)CRUD幌绍,我們可以一鍵生成!
一鍵生成
其實(shí)到上面一節(jié)故响,整個(gè)框架應(yīng)該已經(jīng)符合預(yù)期了傀广!但是為了得到超預(yù)期的效果,我們來(lái)更進(jìn)一步彩届!
我們先看目前的開(kāi)發(fā)流程:
設(shè)計(jì)數(shù)據(jù)表
生成Model,Mapper
編寫(xiě)Param,Result
編寫(xiě)Respository
編寫(xiě)Service
編寫(xiě)Controller
編寫(xiě)測(cè)試
執(zhí)行測(cè)試
提交代碼
對(duì)于一個(gè)典型的CRUD操作伪冰,這里有多少重復(fù)代碼呢?
篇幅有限樟蠕,舉個(gè)簡(jiǎn)單的例子:現(xiàn)在需要編寫(xiě)Order和User的新增邏輯贮聂,Controller的代碼是什么樣的?
Controller:
package ${package.Controller};
import ...
@Api(tags = "${table.controllerName}")
@RestController
@RequestMapping("$!{cfg.basePath}")
public class ${table.controllerName} extends ${superControllerClass}{
@Autowired
private ${table.serviceImplName} ${instanceName}Service;
private Logger logger = LoggerFactory.getLogger(${table.controllerName}.class);
@ApiOperation(value = "創(chuàng)建${entity}")
@RequestMapping(value = "/$!{cfg.version}/${table.entityPath}", method = RequestMethod.POST)
public Result create(@RequestBody @Validated(Create.class) ${entity}Param param, BindingResult bindingResult) {
try {
//驗(yàn)證失敗
if (bindingResult.hasErrors()) {
throw new ValidException(bindingResult.getFieldError().getDefaultMessage());
}
Long recId = ${instanceName}Service.create(param);
return Result.ok(recId);
} catch (BusinessException e) {
logger.error("create ${entity} Error!", e);
return Result.error(e);
} catch (Exception e) {
logger.error("create ${entity} Error!", e);
return Result.error(CommonConstants.SERVER_ERROR, e.getMessage());
}
}
}
如上的模板是否能符合OrderController和UserController寨辩?再往后看Service,Param,Result等是否都可以用類(lèi)似的模板來(lái)統(tǒng)一處理吓懈?
所以,我們完全可以對(duì)相應(yīng)的代碼進(jìn)行自動(dòng)生成捣染,盡可能的降低模板代碼的手動(dòng)編寫(xiě)骄瓣。對(duì)于標(biāo)準(zhǔn)的CRUD邏輯,我們可以做到如下的開(kāi)發(fā)流程:
- 設(shè)計(jì)數(shù)據(jù)表
- 生成CRUD耍攘,包括測(cè)試(我們測(cè)試的是Service榕栏,想想測(cè)試代碼和Controller代碼有多少區(qū)別?)
- 執(zhí)行測(cè)試
- 提交代碼
對(duì)于不可重復(fù)生成的文件蕾各,我們可以設(shè)置"存在即不覆蓋"扒磁,在最大限度的提高開(kāi)發(fā)效率的前提下,降低誤操作式曲。
總結(jié)
如上即是我基于約束所做的Web推導(dǎo)妨托!目前的主要問(wèn)題還是在Model層面:
- 數(shù)據(jù)表映射為Model是否是合理的?
- 基于Model的操作是否合適吝羞?
- 基于上面Param兰伤、Result和Model的關(guān)系圖來(lái)看,實(shí)際上Param钧排、Result和Model大部分情況下都不是契合的敦腔!把這些Param、Result限制在Model上是否合適恨溜?數(shù)據(jù)結(jié)構(gòu)是否清晰符衔?
目前個(gè)人覺(jué)得基于data的transform找前、filter、map操作更適合web開(kāi)發(fā)(我會(huì)另開(kāi)一篇討論這個(gè))判族!或者你有什么好的方案躺盛,歡迎指教?