Web開(kāi)發(fā)框架推導(dǎo)

本文欲回答這樣一個(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)題:

  1. Controller需要對(duì)頁(yè)面?zhèn)鬟^(guò)來(lái)的參數(shù)做哪些操作囊榜?

  2. 頁(yè)面?zhèn)鱽?lái)的參數(shù)和Model是一個(gè)什么關(guān)系审胸?

  3. 從Controller返回給頁(yè)面的數(shù)據(jù)又和Model是什么關(guān)系

  4. 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)系:


50e52024-4589-4069-be6b-297b5d4ebe53-image1.png

從上圖可以看出,除了第一種情況(且這種情況很少)煤搜,其它四種情況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è))判族!或者你有什么好的方案躺盛,歡迎指教?

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末形帮,一起剝皮案震驚了整個(gè)濱河市槽惫,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌辩撑,老刑警劉巖躯枢,帶你破解...
    沈念sama閱讀 218,755評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異槐臀,居然都是意外死亡锄蹂,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén)水慨,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)得糜,“玉大人,你說(shuō)我怎么就攤上這事晰洒〕叮” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,138評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵谍珊,是天一觀(guān)的道長(zhǎng)治宣。 經(jīng)常有香客問(wèn)我,道長(zhǎng)砌滞,這世上最難降的妖魔是什么侮邀? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,791評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮贝润,結(jié)果婚禮上绊茧,老公的妹妹穿的比我還像新娘。我一直安慰自己打掘,他們只是感情好华畏,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,794評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著尊蚁,像睡著了一般亡笑。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上横朋,一...
    開(kāi)封第一講書(shū)人閱讀 51,631評(píng)論 1 305
  • 那天仑乌,我揣著相機(jī)與錄音,去河邊找鬼。 笑死绝骚,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的祠够。 我是一名探鬼主播压汪,決...
    沈念sama閱讀 40,362評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼古瓤!你這毒婦竟也來(lái)了止剖?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,264評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤落君,失蹤者是張志新(化名)和其女友劉穎穿香,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體绎速,經(jīng)...
    沈念sama閱讀 45,724評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡皮获,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了纹冤。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片洒宝。...
    茶點(diǎn)故事閱讀 40,040評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖萌京,靈堂內(nèi)的尸體忽然破棺而出雁歌,到底是詐尸還是另有隱情,我是刑警寧澤知残,帶...
    沈念sama閱讀 35,742評(píng)論 5 346
  • 正文 年R本政府宣布靠瞎,位于F島的核電站,受9級(jí)特大地震影響求妹,放射性物質(zhì)發(fā)生泄漏乏盐。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,364評(píng)論 3 330
  • 文/蒙蒙 一制恍、第九天 我趴在偏房一處隱蔽的房頂上張望丑勤。 院中可真熱鬧,春花似錦吧趣、人聲如沸法竞。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,944評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)岔霸。三九已至,卻和暖如春俯渤,著一層夾襖步出監(jiān)牢的瞬間呆细,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,060評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工八匠, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留絮爷,地道東北人趴酣。 一個(gè)月前我還...
    沈念sama閱讀 48,247評(píng)論 3 371
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像坑夯,于是被迫代替她去往敵國(guó)和親岖寞。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,979評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,162評(píng)論 25 707
  • Spring Cloud為開(kāi)發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見(jiàn)模式的工具(例如配置管理柜蜈,服務(wù)發(fā)現(xiàn)仗谆,斷路器,智...
    卡卡羅2017閱讀 134,659評(píng)論 18 139
  • 手賤淑履,看了不該看的隶垮,又是一晚失眠,明知你這么渣秘噪,我已經(jīng)完全沒(méi)了底線(xiàn)
    滾你丫閱讀 153評(píng)論 0 0
  • 和心愛(ài)的人兒在一起 無(wú)論干什么 都很開(kāi)心 一起起床 一起上班 一起回家 一起買(mǎi)菜 一起做飯 一起看電視 一起聽(tīng)音樂(lè)...
    太不專(zhuān)業(yè)閱讀 212評(píng)論 0 0
  • 原計(jì)劃公務(wù)員狸吞,結(jié)果都沒(méi)準(zhǔn)備。因?yàn)榭傆惺虑榇驍_指煎。 1.幫蔡老師整理調(diào)研組人員名單 2.下午修筆記本電腦捷绒,與連成聊了很...
    炎麗閱讀 261評(píng)論 0 0