摘要:邊界清晰,有利于理解開發測試和部署。前后端分離考慮到目前開發流行前后端分離,為了適應潮流,引入前后端分離的約束。該請求被接受處理,但是該處理是不完整的。
本文欲回答這樣一個問題:在 「特定環境 」下,如何規劃Web開發框架,使其能滿足 「期望 」?
假設我們的「特定環境 」如下:
技術層面
使用Java語言進行開發
通過Maven構建
基于SpringBoot
使用IntellijIDEA作為IDE
使用Mybatis作為持久層框架
前后端分離
非技術層面
新項目,變化較頻繁
快速迭代
開發人員資歷較淺
人員流動性較大
我們的 「期望 」是:
快速上手:鑒于人員流動性較大、開發人員的資歷較淺和項目的快速迭代需求,期望開發框架易于開發人員開發。易于入門,易于部署。
符合行業規約:盡量不定義私有規范,使用行業標準,進一步降低學習難度
快速開發:盡可能復用代碼,盡可能自動化生成模板代碼
獨立性:應用能獨立運行,不過多的依賴其它應用或中間件。邊界清晰,有利于理解、開發、測試和部署。反例:就是沒有規劃的RPC調用。
易于測試:能方便的進行單元/集成測試,不影響真實數據
易于部署:能方便的進行部署,便于快速的擴容
異常可追蹤:對異常,可快速定位到具體是哪個應用,哪個類,哪行代碼的問題
本文從一個空框架開始,逐步加入上面的約束,最終推導出符合期望的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四個包,職責如下:
Controller:接收前臺的請求,驗證數據,組裝需要的數據,委托Service執行具體業務邏輯,并將結果組裝返回給前臺
Service:處理核心業務邏輯,包含事務
Model:數據模型,與數據庫表的對應類
Respository:數據操作類包,操作Model中的類,進行基本的CRUD操作
?
分層后的框架邏輯清晰,且切分方式符合行業規約,更易于上手。
考慮到目前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,需要符合四個核心的約束:
資源的識別(identification of resources)
通過表述操作資源(manipulation of resources through representations)
自描述的消息(self-descriptive messages)
超媒體作為應用狀態引擎(hypermedia as the engine of application state)
絕大部分聲稱符合RESTful的應用都不是百分百符合這四個約束,特別是超媒體作為應用狀態引擎(hypermedia as the engine of application state)這個約束。
?
確定了以JSON的方式進行參數的傳遞后,就需要確定如何來處理參數和返回結果?這涉及到幾個問題:
Controller如何接收參數?
Controller如何返回結果?
Controller如何將數據傳遞給Respository進行持久化處理?
Respository又如何將數據從數據庫中查出來返回給Controller?
這里選擇了Mybatis作為持久化框架,我們先從Mybatis的角度來回答上面的幾個問題!
首先Mybatis作為框架,會生成幾個文件:Model.java,Mapper.java和Mapper.xml!(這里不做過多解釋!對Mybatis不熟悉的朋友請自行google!)這幾個文件可以自動生成,也可以手寫!
不論是自動生成還是手寫都有其優缺點:
先說自動生成的優缺點:
優點就是在修改表結構以后,直接一條命令就可以自動生成新文件。
缺點就是這三個文件不能修改,如果修改了就不能再次自動生成了,否則會被覆蓋。
手動編寫的優缺點:
優點是完全自主控制,可復用Model,在里面添加注解,實現數據驗證、主鍵加解密、字典自動查詢等邏輯。
缺點就是表結構調整后,需要手動修改需要調整的文件。一是繁瑣,二是沒有編譯期校驗,如果手誤寫錯了,直到運行期才可能發現
一種優化方案是,第一次使用自動生成,后續手動修改。
但是結合前面的約束:
新項目,變化較頻繁
快速迭代
開發人員資歷較淺
此方法并不適用。 此方法只對于改動不太頻繁的項目還算適用,但是如果表結構改動較頻繁,后續的每次修改還是要手動修改,非常的麻煩(無法適應頻繁的變更,快速迭代)。且只能第一次使用自動生成這個規定并沒法強制實施,你沒法保證誰不會誤操作了自動生成(考慮開發人員資歷較淺),導致手寫的代碼被覆蓋了!
結合以上約束,為了盡量避免錯誤,優先選擇自動生成!再來嘗試解決其短板,即生成的三個文件無法進行修改。是否有可行方案呢?
我們先考慮幾個問題:
Controller需要對頁面傳過來的參數做哪些操作?
頁面傳來的參數和Model是一個什么關系?
從Controller返回給頁面的數據又和Model是什么關系?
Controller對返回給頁面的數據又要做哪些操作?
為方便起見,我們把入參稱為Param,返回結果稱為Result。我們先回答第一個和第四個問題!
Controller需要對Param做哪些操作?
把從頁面傳遞過來的flat數據transform為對象(這是面向對象語言的一種典型做法,我目前更偏向函數式做法,另開一篇討論)
對數據做校驗:類型對不對、格式對不對、是否為空等等等等
解密:有些字段數據可能是加過密的,比如主鍵,在transform的過程中需要對這些字段進行解密處理
Controller需要對Result做哪些操作?
加密:對需要加密的字段進行加密操作,比如主鍵
字典轉換:有些字段是code碼,頁面需要code碼對應的值,方便人類閱讀。這里需要根據這些code碼從字典中獲取對應的值(你可以在數據庫查詢的時候,直接關聯字典表查詢,但是這樣會帶來兩個麻煩,一個是model中需要包含字典value字段,就沒法自動生成了。第二個就是,一般字典會放在內存中,關聯表查詢相對內存取數據,性能上會有劣勢)
字典列表:和字典轉換類似,有些頁面需要字典列表數據,需要獲取這些數據到前臺供用戶選擇
這些操作都可以方便的處理:
SpringMVC已經提供了數據綁定功能,將數據綁定到對象上
JSR303基于注解進行校驗
加解密、字典都可以通過自定義注解處理(擴展Jackson的注解處理即可。Jackson的注解只在方法上生效,本以為是個問題,卻助我構思了一個方案:一個結合了自動生成的方便性和手寫的靈活性的方案!!!!)
這些都是規約!
針對第二個和第三個問題,我們先看Param、Result和Model之間的關系:
從上圖可以看出,除了第一種情況(且這種情況很少),其它四種情況Param和Model實際是一個包含的關系。既然是一種包含的情況,那這種包含關系,在Java里我們可以使用繼承來實現。也就是說可以使Param extends Model,以這樣的方式來復用Model的內容!
我們來看以這種方式來實現Param和Result,如何來解決上面的問題!
首先,因為Param和Result都繼承了Model,所以Model是不需要做任何改動的,就可以無限次的自動生成
其次,數據驗證、加解密的注解是可以添加到方法上的。我們對需要這些注解的字段,在Param/Result里覆蓋Model里的get/set方法,在其上添加注解,就可以使用基于注解的數據驗證和加解密。
假設數據字段有了修改,重新生成后,由于有@Override注解,在編譯期就可以定位到需要修改的get/set方法,結合IDE可以快速修復
如果是新增字段,則直接重新生成Mybatis的三個文件即可,原有代碼不受任何影響
?
盡量以擴展規約的方式來處理問題,在不增加理解難度的情況下提高易用性和開發效率!
在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測試,原因如下:
在上面的分層架構里描述了各層的職責,可以看出,核心業務都在Service層,Controller和Model都沒有業務邏輯,只是一些標準化代碼,沒必要測試
SpringBoot對Controller的測試是在不同的線程內,不支持事務,如果在正式環境測試的話,會影響正式庫數據
部署SpringBoot可以直接打包為jar包,直接運行啟動。這很方便,但是如果想快速的橫向擴容,配置文件就是一個問題。因為不同機器上的配置并不是完全相同的。
有兩個方案可以解決:
Docker
配置服務器
從便利性考慮,還是選擇配置服務器。
配置文件中均是開發環境配置,方便開發人員直接開發、測試。
在正式環境中,應用啟動時會從配置服務器獲取對應的配置,覆蓋本地測試進行部署。
在結束之前,先問個問題?你是喜歡代碼生成、還是封裝?
代碼生成就類似Mybatis這樣生成了對應的文件,邏輯透明。你可以去改
封裝就類似Hibernate,你寫個對象,然后對對象操作就行了,底層數據庫操作由Hibernate來處理
我個人更偏向代碼生成,理由是:
簡單:易于使用,易于上手
行業標準:生成的代碼是行業標準代碼,只要熟悉Mybatis,Spring就可以直接上手(而Mybatis和Spring目前是互聯網標配)。如果公司內部進行一些封裝,那么新手需要先理解這些封裝,增加了學習成本。
基于上面的原因,再考慮到其實我們的框架都是符合規約的(RESTful,JSR303,覆寫,Jackson),故對于標準CRUD,我們可以一鍵生成!
一鍵生成其實到上面一節,整個框架應該已經符合預期了!但是為了得到超預期的效果,我們來更進一步!
我們先看目前的開發流程:
設計數據表
生成Model,Mapper
編寫Param,Result
編寫Respository
編寫Service
編寫Controller
編寫測試
執行測試
提交代碼
對于一個典型的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邏輯,我們可以做到如下的開發流程:
設計數據表
生成CRUD,包括測試(我們測試的是Service,想想測試代碼和Controller代碼有多少區別?)
執行測試
提交代碼
對于不可重復生成的文件,我們可以設置"存在即不覆蓋",在最大限度的提高開發效率的前提下,降低誤操作。
總結如上即是我基于約束所做的Web推導!目前的主要問題還是在Model層面:
數據表映射為Model是否是合理的?
基于Model的操作是否合適?
基于上面Param、Result和Model的關系圖來看,實際上Param、Result和Model大部分情況下都不是契合的!把這些Param、Result限制在Model上是否合適?數據結構是否清晰?
目前個人覺得基于data的transform、filter、map操作更適合web開發(我會另開一篇討論這個)!或者你有什么好的方案,歡迎指教?
公眾號:ivaneye
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/68684.html
摘要:與的特點比較這兩個目前都是小眾語言做了些時間的研究寫了點東西有了點心得相似點有衛生宏區別與的不衛生宏在類或定義體之外定義函數代碼沒有分成頭與實現體例如的頭與實現的與定義的接口定義與實現定義是分開的而與是不分開的運用函數式編程高階函數目前是新 nim與rust的特點比較 這兩個目前都是小眾語言,做了些時間的研究,寫了點東西有了點心得 相似點: 有衛生宏.區別與C++的(不衛生)宏 在類...
摘要:前言本文系統的梳理了搭建項目的常見用法,目的在于讓你快速掌握獨立搭建項目的能力。思維導圖接下來,我們根據思維導圖,一步步來解釋和實現我們的目標。這確保了最終包里數量的最小化。但是如果其中一個依賴需要特殊的,默認情況下無法將其檢測出來。 前言 本文系統的梳理了vue-cli3搭建項目的常見用法,目的在于讓你快速掌握獨立搭建vue項目的能力。你將會了解如下知識點: 如何安裝項目插件 添加...
摘要:比如或者都會導致函數返回值類型時。和特性一樣,等于是函數返回值中的或。注意對比下面的寫法對于,它的返回值是可迭代的對象,并且每個類型都是或者。首先是不支持方法重載的,是支持的,而類型系統一定程度在對標,當然要支持這個功能。 1 引言 精讀原文是 typescript 2.0-2.9 的文檔: 2.0-2.8,2.9 草案. 我發現,許多寫了一年以上 Typescript 開發者,對 T...
摘要:蠎周刊年度最贊親俺們又來回顧又一個偉大的年份兒包去年最受歡迎的文章和項目如果你錯過了幾期就這一期不會丟失最好的嗯哼還為你和你的準備了一批紀念裇從這兒獲取任何時候如果想分享好物給大家在這兒提交喜歡我們收集的任何意見建議通過來吧原文 Title: 蠎周刊 2015 年度最贊Date: 2016-01-09 Tags: Weekly,Pycoder,Zh Slug: issue-198-to...
摘要:不幸的是,在軟件包管理十分混亂,至少歷史上十分混亂。的最大改進是將函數的參數單獨放到一個的文件中這些成為包的元數據。基于的版本號管理。的版本推導這里重點說明一下基于的版本號管理這個功能。開發版本號的形式如下。 為什么寫這個系列 OpenStack是目前我所知的最大最復雜的基于Python項目。整個OpenStack項目包含了數十個主要的子項目,每個子項目所用到的庫也不盡相同。因此,對于...
閱讀 2984·2021-10-19 11:46
閱讀 978·2021-08-03 14:03
閱讀 2934·2021-06-11 18:08
閱讀 2904·2019-08-29 13:52
閱讀 2744·2019-08-29 12:49
閱讀 479·2019-08-26 13:56
閱讀 924·2019-08-26 13:41
閱讀 848·2019-08-26 13:35