本文欲回答這樣一個問題:在 「特定環境 」下,如何規劃Web開發框架,使其能知足 「指望 」?java
假設咱們的「特定環境 」以下:web
技術層面spring
非技術層面數據庫
咱們的 「指望 」是:後端
本文從一個空框架開始,逐步加入上面的約束,最終推導出符合指望的Web框架!
本文提供的是一種思路!若有紕漏、或不一樣意見,歡迎討論指正!緩存
咱們從一個「空框架」開始咱們的框架推導!所謂「空框架」是一個沒有任何約束的接收HTTP的可運行代碼,好比對任何請求都只返回Hello World的servlet!
這裏咱們基於Maven和SpringBoot快速搭建一個「空框架」!服務器
代碼結構以下(Maven構建約束): 網絡
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); } }
啓動後,當訪問http://localhost:8080時,頁面上將顯示Hello world!字樣!架構
咱們徹底能夠基於這個「空框架」進行開發,可是這個「空框架」離咱們的指望還很遠。咱們來一步步的改造!
分層架構能夠說是Web項目的默認架構風格,能夠說是行業標準!因此咱們首先引入分層架構這個約束!
分層架構有其優點和劣勢:
Web裏最經常使用的切分方式就是MVC模式!咱們對咱們的「空框架」引入MVC模式!
那咱們這裏是切分包?仍是切分模塊呢?考慮到最小影響原則,這裏先切分包。若是有後續約束,再作進一步調整。
引入MVC模式後的代碼結構:
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!"; } }
這裏暫時切分了Controller,Service,Model,Respository四個包,職責以下:
分層後的框架邏輯清晰,且切分方式符合行業規約,更易於上手。
考慮到目前Web開發流行先後端分離,爲了適應潮流,引入先後端分離的約束。
爲了適應先後端分離,後端不負責頁面的渲染,只接收和返回JSON數據。SpringBoot對此有直接的支持,直接將@Controller改成@RestController便可!
相關代碼:
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!"; } }
整個URL符合RESTful,即符合行業規約!至於REST相關內容另行討論。
實際上完整的RESTful應用不僅是URL符合RESTful,須要符合四個核心的約束:
絕大部分聲稱符合RESTful的應用都不是百分百符合這四個約束,特別是超媒體做爲應用狀態引擎(hypermedia as the engine of application state)這個約束。
肯定了以JSON的方式進行參數的傳遞後,就須要肯定如何來處理參數和返回結果?這涉及到幾個問題:
這裏選擇了Mybatis做爲持久化框架,咱們先從Mybatis的角度來回答上面的幾個問題!
首先Mybatis做爲框架,會生成幾個文件:Model.java,Mapper.java和Mapper.xml!(這裏不作過多解釋!對Mybatis不熟悉的朋友請自行google!)這幾個文件能夠自動生成,也能夠手寫!
不管是自動生成仍是手寫都有其優缺點:
先說自動生成的優缺點:
手動編寫的優缺點:
一種優化方案是,第一次使用自動生成,後續手動修改。
可是結合前面的約束:
此方法並不適用。 此方法只對於改動不太頻繁的項目還算適用,可是若是表結構改動較頻繁,後續的每次修改仍是要手動修改,很是的麻煩(沒法適應頻繁的變動,快速迭代)。且只能第一次使用自動生成這個規定並無法強制實施,你無法保證誰不會誤操做了自動生成(考慮開發人員資歷較淺),致使手寫的代碼被覆蓋了!
結合以上約束,爲了儘可能避免錯誤,優先選擇自動生成!再來嘗試解決其短板,即生成的三個文件沒法進行修改。是否有可行方案呢?
咱們先考慮幾個問題:
爲方便起見,咱們把入參稱爲Param,返回結果稱爲Result。咱們先回答第一個和第四個問題!
Controller須要對Param作哪些操做?
Controller須要對Result作哪些操做?
這些操做均可以方便的處理:
這些都是規約!
針對第二個和第三個問題,咱們先看Param、Result和Model之間的關係:
從上圖能夠看出,除了第一種狀況(且這種狀況不多),其它四種狀況Param和Model實際是一個包含的關係。既然是一種包含的狀況,那這種包含關係,在Java裏咱們可使用繼承來實現。也就是說可使Param extends Model,以這樣的方式來複用Model的內容!
咱們來看以這種方式來實現Param和Result,如何來解決上面的問題!
儘可能以擴展規約的方式來處理問題,在不增長理解難度的狀況下提升易用性和開發效率!
在RESTful約束中,推薦使用HTTP的標準響應來處理返回數據。SpringMVC中也提供了標準響應的支持。
ResponseEntity.ok("body"); ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("");
可是因爲HTTP的標準狀態碼太少了,見下表:
代碼 | 消息 | 描述 |
---|---|---|
100 | Continue | 只有請求的一部分已經被服務器接收,但只要它沒有被拒絕,客戶端應繼續該請求。 |
101 | Switching Protocols | 服務器切換協議。 |
200 | OK | 請求成功。 |
201 | Created | 該請求是完整的,並建立一個新的資源。 |
202 | Accepted | 該請求被接受處理,可是該處理是不完整的。 |
203 | Non-authoritative Information | |
204 | No Content | |
205 | Reset Content | |
206 | Partial Content | |
300 | Multiple Choices | 連接列表。用戶能夠選擇一個連接,進入到該位置。最多五個地址 |
301 | Moved Permanently | 所請求的頁面已經轉移到一個新的 URL。 |
302 | Found | 所請求的頁面已經臨時轉移到一個新的 URL。 |
303 | See Other | 所請求的頁面能夠在另外一個不一樣的 URL 下被找到。 |
304 | Not Modified | |
305 | Use Proxy | |
306 | Unused | 在之前的版本中使用該代碼。如今已再也不使用它,但代碼仍被保留。 |
307 | Temporary Redirect | 所請求的頁面已經臨時轉移到一個新的 URL。 |
400 | Bad Request | 服務器不理解請求。 |
401 | Unauthorized | 所請求的頁面須要用戶名和密碼。 |
402 | Payment Required | 你還不能使用該代碼。 |
403 | Forbidden | 禁止訪問所請求的頁面。 |
404 | Not Found | 服務器沒法找到所請求的頁面。 |
405 | Method Not Allowed | 在請求中指定的方法是不容許的。 |
406 | Not Acceptable | 服務器只生成一個不被客戶端接受的響應。 |
407 | Proxy Authentication Required | 在請求送達以前,您必須使用代理服務器的驗證。 |
408 | Request Timeout | 請求須要的時間比服務器可以等待的時間長,超時。 |
409 | Conflict | 請求由於衝突沒法完成。 |
410 | Gone | 所請求的頁面再也不可用。 |
411 | Length Required | "Content-Length" 未定義。服務器沒法處理客戶端發送的不帶 Content-Length 的請求信息。 |
412 | Precondition Failed | 請求中給出的先決條件被服務器評估爲 false。 |
413 | Request Entity Too Large | 服務器不接受該請求,由於請求實體過大。 |
414 | Request-url Too Long | 服務器不接受該請求,由於 URL 太長。當你轉換一個 「post」 請求爲一個帶有長的查詢信息的 「get」 請求時發生。 |
415 | Unsupported Media Type | 服務器不接受該請求,由於媒體類型不被支持。 |
417 | Expectation Failed | |
500 | Internal Server Error | 未完成的請求。服務器遇到了一個意外的狀況。 |
501 | Not Implemented | 未完成的請求。服務器不支持所需的功能。 |
502 | Bad Gateway | 未完成的請求。服務器從上游服務器收到無效響應。 |
503 | Service Unavailable | 未完成的請求。服務器暫時超載或死機。 |
504 | Gateway Timeout | 網關超時。 |
505 | HTTP Version Not Supported | 服務器不支持「HTTP協議」版本。 |
這些標準的狀態碼沒法詳細的表示一個項目中的全部狀況。且目前SpringMVC不支持自定義狀態碼。就是相似這樣的代碼:
ResponseEntity.status(10001).body("");
雖然不報錯,可是沒法正常響應,後臺會報相似「非標準狀態碼」的錯誤!
因此我自定義了一個對象Result,用來完成相似ResponseEntity的工做。Result的結構以下:
public class Result { private int code;//200爲正常,其它爲相關業務報錯 private String msg;//對應的錯誤信息,200爲ok private Object body;//返回的業務對象 }
提供相似:
Result.ok("body") Result.error(e); Result.error(CommonConstants.SERVER_ERROR, e.getMessage());
這樣的構造方法,方便使用。
異常處理在上面數據返回裏涉及了一點(就是Result的構造以及業務的各類場景處理)。這裏詳細說明。
約束中須要能方便的追蹤異常!
Java裏提供了CheckedException和UnCheckedException,而對於咱們實際使用來講,仍是須要區分業務場景。
異常是業務異常仍是非業務異常?
表現到代碼上,對於業務異常咱們能夠定義BusinessException來表示,全部繼承了BusinessException的異常,都是業務異常,而其它異常就是非業務異常。
更進一步,業務異常也能夠分爲:
這兩種異常,咱們能夠經過異常碼來區分,例如:100開頭的爲通用業務異常,300開頭的爲訂單異常,400開頭的爲產品異常,依此類推。
同時異常的Code和Msg與Result對應,方便構建Result.error(e);直接返回。
再進一步,目前的應用都是分佈式的,甚至是微服務架構!咱們是否能夠經過異常能快速的定位到是哪一個應用的哪一個模塊裏的哪一個代碼出問題了呢?
一種可行方案仍是經過異常碼來處理:以三位數字爲間隔,來區分應用+模塊+代碼,例如:001002301,能夠理解爲異常是001機器上的,002應用,拋出的301(訂單相關)異常。
當系統變得愈來愈大後,不免不會出現系統內不一樣應用之間的相互調用;若是是微服務的話,那麼服務間的相互調用是很常見的。若是處理不當,會使得各應用之間相互依賴,沒法獨立的運行。致使開發、測試、部署都很麻煩。
爲了不這樣的問題出現,結合以下兩個約束:
故使用RESTful方式,做爲應用間通訊的方式。這也是微服務推薦的通訊方式!
應用間調用會出現Model的依賴,故這裏將Model從包提高爲模塊。方便後續若是有其它應用要依賴時,可直接依賴Model模塊,而不是整個應用。
調整後代碼結構以下:
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包移動到了intellijweb2-model模塊中,同時新增了param和result包!
SpringBoot自己提供了較爲完善的測試功能。包括單元測試、Mocker、Spy等。
基於以下幾個考慮:
故決定只對Service測試,緣由以下:
SpringBoot能夠直接打包爲jar包,直接運行啓動。這很方便,可是若是想快速的橫向擴容,配置文件就是一個問題。由於不一樣機器上的配置並非徹底相同的。
有兩個方案能夠解決:
從便利性考慮,仍是選擇配置服務器。
配置文件中均是開發環境配置,方便開發人員直接開發、測試。
在正式環境中,應用啓動時會從配置服務器獲取對應的配置,覆蓋本地測試進行部署。
在結束以前,先問個問題?你是喜歡代碼生成、仍是封裝?
我我的更偏向代碼生成,理由是:
基於上面的緣由,再考慮到其實咱們的框架都是符合規約的(RESTful,JSR303,覆寫,Jackson),故對於標準CRUD,咱們能夠一鍵生成!
其實到上面一節,整個框架應該已經符合預期了!可是爲了獲得超預期的效果,咱們來更進一步!
咱們先看目前的開發流程:
對於一個典型的CRUD操做,這裏有多少重複代碼呢?
篇幅有限,舉個簡單的例子:如今須要編寫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 = "建立${entity}") @RequestMapping(value = "/$!{cfg.version}/${table.entityPath}", method = RequestMethod.POST) public Result create(@RequestBody @Validated(Create.class) ${entity}Param param, BindingResult bindingResult) { try { //驗證失敗 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等是否均可以用相似的模板來統一處理?
因此,咱們徹底能夠對相應的代碼進行自動生成,儘量的下降模板代碼的手動編寫。對於標準的CRUD邏輯,咱們能夠作到以下的開發流程:
對於不可重複生成的文件,咱們能夠設置"存在即不覆蓋",在最大限度的提升開發效率的前提下,下降誤操做。
如上便是我基於約束所作的Web推導!目前的主要問題仍是在Model層面:
目前我的以爲基於data的transform、filter、map操做更適合web開發(我會另開一篇討論這個)!或者你有什麼好的方案,歡迎指教?
公衆號:ivaneye