團(tuán)隊開發(fā)框架實戰(zhàn)—CQRS架構(gòu)

團(tuán)隊開發(fā)框架實戰(zhàn)—CQRS架構(gòu)

CQRS架構(gòu)圖

261851438603372.jpg
CQRS架構(gòu)圖.png

什么是CQRS?

這里只通過Udi Dahan的《Clarified CQRS》文章中的一張圖片簡要介紹一下:


2012032222580035.png

UI上有兩種類型的操作:命令和查詢,例如顯示銷量最好的5個產(chǎn)品就屬于查詢,而提交一個訂單娩嚼、修改密碼等則屬于命令。因為大部分系統(tǒng)都是讀多寫少贴铜,而且業(yè)務(wù)邏輯基本都出現(xiàn)在寫入的一端逊躁,所以查詢和命令的分離可以讓我們獨立的去優(yōu)化查詢。

查詢 (Query)

上圖中分别,可以看到Query不是通過DB來查詢淮菠,而是通過一個專門用于查詢的Read DB(上圖中的Cache男公,它不一定是數(shù)據(jù)庫,但為方便起見合陵,下面統(tǒng)稱Read DB)枢赔,Read DB中的表(方便起見澄阳,暫且認(rèn)為這個Read DB是一個RDBMS)是專門針對UI優(yōu)化過的,例如里面可能會有LatestProductListModel(ProductId, ProductName, Price, BrandName, AddedTime)踏拜、BestSoldProductListModel(ProductId, ProductName, TotalSold)這樣的表碎赢,分別表示最新的產(chǎn)品列表,銷量最好的產(chǎn)品列表(它們其實就相當(dāng)于是View Model)执隧。LatestProductListModel中有一個BrandName的字段揩抡,注意,不是BrandId镀琉,因此峦嗤,對于界面中的查詢,幾乎全都可以通過SELECT * FROM [TABLE]這樣的SQL語句來實現(xiàn)屋摔,可能有少數(shù)Where烁设,但基本沒有Join,這對于界面的加載速度絕對是有利無弊的(其實也是在用空間換時間)钓试。

命令 (Command)

業(yè)務(wù)邏輯大部分都發(fā)生在寫入的時候装黑,例如用戶購買商品提交訂單時,我們要驗證庫存弓熏,用戶信息訂單數(shù)據(jù)是否有效等恋谭。如果從傳統(tǒng)DDD的角度看,Command類似于Application Service挽鞠,用戶的命令(如提交訂單)會以Command的形式得到執(zhí)行疚颊,而Command中也不會帶有業(yè)務(wù)邏輯,Command中做的事情基本上是:通過Repository得到相關(guān)的領(lǐng)域?qū)ο笮湃希{(diào)用某些領(lǐng)域服務(wù)(Domain Service)執(zhí)行一些操作(業(yè)務(wù)邏輯都將保留在領(lǐng)域模型中)材义,然后執(zhí)行Commit或SaveChanges之類的方法提交改動,之后嫁赏,相關(guān)的數(shù)據(jù)就會寫入到Write DB中(圖的DB其掂,下文統(tǒng)稱Write DB)。需要注意的是潦蝇,UI上的查詢都是查Read DB款熬,而不是Write DB。

領(lǐng)域模型 (Domain Model)

這和Evans的DDD中說的領(lǐng)域模型沒有太多區(qū)別护蝶,是“the heart of software”华烟。

領(lǐng)域事件 (Domain Event)

領(lǐng)域事件占據(jù)的地位非常重要,不僅限于CQRS持灰。相信會有一部分人曾和我一樣碰到過這樣的問題:
Account實體(表示帳戶)有個Balance屬性(表示帳戶余額),我們一般不會公開這個屬性的setter负饲,而是通過寫一些IncreaseBalance(decimal amount)之類的方法來實現(xiàn)帳戶余額的變動堤魁。
這時問題就來了喂链,我們想在帳戶變動時添加一條AccountLog記錄,但Log記錄成千上萬妥泉,我們不能直接通過ORM的一對多映射把AccountLog集合實現(xiàn)成Account的一個集合屬性椭微,那我們就需要在IncreaseBalance()中得到AccountLogRepository,這樣才有辦法插入AccountLog(從DDD的角度盲链,AccountLog不是聚合根蝇率,所以不能有AccountLogRepository,但在性能影響嚴(yán)重的時候刽沾,也只好做些取舍了)本慕。
不管用了依賴注入還是什么的,總之侧漓,Account已經(jīng)依賴上Repository了锅尘,這就讓領(lǐng)域?qū)ο笞兊煤懿患儍簦⑶也颊幔偃缥覀円院蟛粌H要記錄log藤违,還要短信通知用戶呢?那要修改源代碼嗎纵揍?這也很不OCP顿乒。
而領(lǐng)域事件正好可以解決這種問題:只要在IncreaseBalance()方法的末尾,觸發(fā)一個領(lǐng)域事件泽谨,然后我們獨立寫一個EventHandler的類去實現(xiàn)log的添加(框架可以保證EventHandler可以和領(lǐng)域事件綁定到一起)璧榄。
回到CQRS,因為Command將數(shù)據(jù)寫到了Write DB中隔盛,而UI查詢的是Read DB犹菱,那我們就需要用某種方式實現(xiàn)這兩個數(shù)據(jù)庫的同步,解決辦法已經(jīng)很明顯了吮炕,寫一堆的EventHandler類去監(jiān)聽領(lǐng)域事件腊脱。例如我們有一個更改產(chǎn)品價格的命令ChangePriceCommand,它執(zhí)行后龙亲,一個叫做PriceChangedEvent會被觸發(fā)陕凹,那我們只要寫一個PirceChangedEventHandler的類,在這里面將Read DB中相關(guān)的價格信息更改到最新值即可實現(xiàn)同步(這里會涉及到Read DB中表結(jié)構(gòu)改變的問題鳄炉,后面再說)杜耙。

Command的實現(xiàn)

概述

UI中的寫入操作都將被封裝為一個命令中,發(fā)送給Domain Model來處理拂盯。
我們遵循Domain Driven Design的設(shè)計思想佑女,因此所有的業(yè)務(wù)邏輯都只在Domain Model中處理,Command中將不會帶有業(yè)務(wù)邏輯。Command中的代碼無非是通過Repository獲取某些個聚合根(Aggregate Root)团驱,然后將操作委托給相應(yīng)的領(lǐng)域?qū)ο蠡蝾I(lǐng)域服務(wù)來處理摸吠,僅此而已。

實現(xiàn)

實現(xiàn)上嚎花,我們會涉及三個東西:

  • Command對象

Command對象的作用是用來封裝命令數(shù)據(jù)寸痢,所以這類對象以屬性為主,少量簡單方法紊选,但注意這些方法中不能包含業(yè)務(wù)邏輯啼止。
舉個用戶注冊的例子,用戶注冊是一個命令兵罢,所以我們需要一個RegisterCommand類献烦,這個類定義如下:

using Tdf.CQRS.Commanding;

namespace Tdf.CQRSSample.Commands
{
    public class RegisterCommand : ICommand
    {
        public string Email { get; set; }
        public string NickName { get; set; }
        public string Password { get; set; }
        public string ConfirmPassword { get; set; }

        public RegisterCommand()
        {
        }
    }
}

這個類的每個屬性基本上都對應(yīng)著注冊表單中的一個輸入(為了方便起見,上面的每個屬性都是public set趣些,但若屬性不多不影響編碼仿荆,最好把屬性都改成private set,然后將屬性的值通過構(gòu)造函數(shù)傳入)坏平。當(dāng)用戶點擊“注冊”按鈕時拢操,Controller(假設(shè)使用MVC作為表現(xiàn)層模式)中會創(chuàng)建一個RegisterCommand的實例,設(shè)置相應(yīng)的值舶替,然后調(diào)用CommandBus.Send(registerCommand)令境,然后根據(jù)執(zhí)行的情況顯示相應(yīng)的信息給用戶。(CommandBus后面會講到)

  • CommandExecutor

CommandExecutor的作用是執(zhí)行一個命令顾瞪,對于注冊的例子舔庶,我們會有一個RegisterCommandExecutor的類,它只有一個Execute方法陈醒,接受RegisterCommand參數(shù):

using System;
using Tdf.CQRS.Commanding;
using Tdf.CQRS.Data;
using Tdf.CQRSSample.Domain.Entities;
using Tdf.CQRSSample.Domain.Services;

namespace Tdf.CQRSSample.Commands
{
    class RegisterCommandExecutor : ICommandExecutor<RegisterCommand>
    {
        public IRepository<User> _repository;

        public RegisterCommandExecutor(IRepository<User> repository)
        {
            _repository = repository;
        }

        public void Execute(RegisterCommand cmd)
        {
            if (String.IsNullOrEmpty(cmd.Email))
                throw new ArgumentException("Email is required.");

            if (cmd.Password != cmd.ConfirmPassword)
                throw new ArgumentException("Password not match.");

            // other command validation logics

            var service = new RegistrationService(_repository);
            service.Register(cmd.Email, cmd.NickName, cmd.Password);
        }
    }
}

在Execute方法中惕橙,我們需要先驗證Command的正確性,但需要注意的是钉跷,這里的驗證只是驗證RegisterCommand中的數(shù)據(jù)是否合法弥鹦,并非驗證業(yè)務(wù)邏輯。例如爷辙,這里會驗證郵箱是否為空且格式是否正確彬坏,但郵箱格式正確并不意味著就可以注冊,因為系統(tǒng)可能要求18歲以上的成年人才能注冊膝晾,而這屬于業(yè)務(wù)邏輯栓始,RegistrationService將會負(fù)責(zé)確保所有的業(yè)務(wù)規(guī)則不被破壞,RegistrationService屬于Domain Service血当,存在于Domain Model中幻赚。

可以看到禀忆,CommandExecutor中主要有兩部分工作,一是驗證傳入的Command對象是否合法坯屿,二是調(diào)用領(lǐng)域模型完成操作油湖。上一篇文章中提到的Command是一個概念層次的Command巍扛,它不單指(1)中的Command领跛,而是包含了(1)和(2)等。

  • Command Bus

用于執(zhí)行Command的是CommandExecutor撤奸,但CommandExecutor卻并不用來在UI層調(diào)用吠昭,UI層中只會用到Command對象和即將提到的Command Bus。Command Bus的作用是將一個Command派發(fā)給相應(yīng)的CommandExecutor去執(zhí)行胧瓜。在開發(fā)UI層時矢棚,我們不需要關(guān)心Command會被哪個Executor執(zhí)行了,而只要知道府喳,上帝賜予了我們一個CommandBus蒲肋,我們只要創(chuàng)建好Command對象,扔給它钝满,神奇的CommandBus就會幫我們把它執(zhí)行完兜粘。這樣一來,對于UI層的開發(fā)來說弯蚜,所涉及的概念很簡單孔轴,涉及的類也少,大部分的工作都是得到表單中的輸入碎捺,封裝成Command對象路鹰,扔給CommandBus。

CommandBus的實現(xiàn)也很簡單收厨。首先晋柱,我們需要讓CommandExecutor都實現(xiàn)一個泛型接口:

namespace Tdf.CQRS.Commanding
{
    public interface ICommandExecutor<TCommand>
        where TCommand : ICommand
    {
        void Execute(TCommand cmd);
    }
}

其中ICommand是一個空接口,沒有任何方法(即Marker Interface)诵叁,它的作用是實現(xiàn)編譯時約束雁竞,這樣我們可以限制傳入CommandExecutor的都是Command對象,而不是不小心傳錯的User對象(所有的Command對象都必須實現(xiàn)ICommand接口)黎休。

然后浓领,把CommandBus寫成這樣:
通過IoC框架來簡化這個過程,另外也可以做一些改進(jìn)势腮,例如將CommandBus設(shè)計為擴(kuò)展點之一联贩。另外我們還可以將UnitOfWork(相當(dāng)于平常的EntityFramework中的IDbContext,Linq 2 SQL中的DataContext)的生命周期在CommandBus中進(jìn)行控制捎拯。
比較完整的CommandBus代碼如下

namespace Tdf.CQRS.Commanding
{
    public interface ICommandBus
    {
        void Send<TCommand>(TCommand cmd) where TCommand : ICommand;
    }
}
using Tdf.CQRS.Data;

namespace Tdf.CQRS.Commanding
{
    public class CommandBus : ICommandBus
    {
        public void Send<TCommand>(TCommand cmd) where TCommand : ICommand
        {
            try
            {
                var unitOfWork = UnitOfWork.StartUnitOfWork();
                var executor = ObjectContainer.Resolve<ICommandExecutor<TCommand>>();
                executor.Execute(cmd);
                UnitOfWork.Commit();
            }
            finally
            {
                UnitOfWork.Close();
            }
        }
    }
}

一些注意點

  • Command表示想要執(zhí)行的命令泪幌,所以Command類的類名應(yīng)當(dāng)是動詞的形式。例如RegisterCommand, ChangePasswordCommand等。不過Command后綴則是可選的祸泪,只要能保持一致即可吗浩。
  • Command和CommandExecutor是一一對應(yīng)的。也就是說没隘,一個Command只會對應(yīng)一個CommandExecutor懂扼,這和后面的事件有區(qū)別,事件是一對多的右蒲,一個Event可以對應(yīng)多個EventHandler阀湿。
  • Command對象也起到了DTO(Data Transfer Object,在這個例子中感覺稱作View Model也無妨)的作用瑰妄,這也是把Command和Executor相分離陷嘴,不把Execute方法直接寫在Command類中的原因之一。
  • 注意Command的類名的重要作用间坐,每個Command類的名稱都清晰地表達(dá)了一個意圖灾挨,例如ChangePasswordCommand清晰的表達(dá)了這個命令是要修改密碼,所以千萬不要隨意"復(fù)用"Command竹宋,這里的“復(fù)用”指的是劳澄,看到某兩個Command中有完全一樣的屬性,就覺得沒有必要使用兩個Command逝撬,而把它們合并成一個Command浴骂,這樣的"復(fù)用"會讓系統(tǒng)變得越來越難以理解,雖然它可能的確減少了幾行代碼宪潮。
  • 命令通常是用“發(fā)送”來描述溯警,而事件則是用“發(fā)布”來描述,所以CommandBus中的方法名稱個人認(rèn)為應(yīng)該用Send比較合適狡相,而不用Publish之類的梯轻。

Command執(zhí)行結(jié)果的返回

面對UI中的各種命令,Controller會創(chuàng)建相應(yīng)的Command對象尽棕,然后將其交給CommandBus喳挑,由CommandBus統(tǒng)一派發(fā)到相應(yīng)的CommandExecutor中去執(zhí)行,我們的ICommandBus的接口聲明如下:

namespace Tdf.CQRS.Commanding
{
    public interface ICommandBus
    {
        void Send<TCommand>(TCommand cmd) where TCommand : ICommand;
    }
}

當(dāng)在實際項目中應(yīng)用CQRS時滔悉,我們會發(fā)現(xiàn)上面的做法存在一個問題:有時候我們希望Command在執(zhí)行完后返回一些結(jié)果伊诵,但上面的Send方法返回void,也就意味著我們沒有辦法得到執(zhí)行結(jié)果回官。我們以一個用戶注冊的例子來說明曹宴。
在Command對象中添加一個ExecutionResult的屬性(這個屬性要放在具體的Command類中,不要放于ICommand接口中)歉提。如上面的用戶注冊的例子笛坦,我們可以添加一個RegisterCommandResult的類区转,然后將RegisterCommand改成如下所示:

using Tdf.CQRS.Commanding;

namespace Tdf.CQRSSample.Commands
{
    public class RegisterCommand : ICommand
    {
        public string Email { get; set; }
        public string NickName { get; set; }
        public string Password { get; set; }
        public string ConfirmPassword { get; set; }

        // 亮點在這里
        public RegisterCommandResult ExecutionResult { get; set; }

        public RegisterCommand()
        {
        }
    }

    // 亮點在這里
    public class RegisterCommandResult
    {
        public string GeneratedUserId { get; set; }
    }
}

在調(diào)用CommandBus.Send()之前,我們完全不用理會這個ExecutionResult屬性版扩,對于Controller的開發(fā)人員來說废离,他只要知道在Command執(zhí)行完后,ExecutionResult的值就會被賦上礁芦,如果沒有蜻韭,那就是CommandExecutor的bug。

而我們的RegisterCommandExecutor就可以改成(User類的構(gòu)造函數(shù)會調(diào)用Id = Guid.NewGuid().ToString()對自己的Id進(jìn)行賦值):

using System;
using Tdf.CQRS.Commanding;
using Tdf.CQRS.Data;
using Tdf.CQRSSample.Domain.Entities;
using Tdf.CQRSSample.Domain.Services;

namespace Tdf.CQRSSample.Commands
{
    class RegisterCommandExecutor : ICommandExecutor<RegisterCommand>
    {
        public IRepository<User> _repository;

        public RegisterCommandExecutor(IRepository<User> repository)
        {
            _repository = repository;
        }

        public void Execute(RegisterCommand cmd)
        {
            if (String.IsNullOrEmpty(cmd.Email))
                throw new ArgumentException("Email is required.");

            if (cmd.Password != cmd.ConfirmPassword)
                throw new ArgumentException("Password not match.");

            // other command validation logics

            var service = new RegistrationService(_repository);
            var user = service.Register(cmd.Email, cmd.NickName, cmd.Password);

            // 亮點在這里
            cmd.ExecutionResult = new RegisterCommandResult
            {
                GeneratedUserId = user.Id
            };
        }
    }
}

RegisterCommand中定義的ExecutionResult屬性可以讓開發(fā)人員清楚的知道這個屬性會在Command執(zhí)行完后被賦上合適的值宴偿。對于一個Command湘捎,如果開發(fā)人員在其中找到類似ExecutionResult這樣的屬性,他就知道這個Command執(zhí)行完后會返回執(zhí)行結(jié)果窄刘,并且結(jié)果是以賦值的形式賦給Command中的ExecutionResult屬性,若Command中沒有發(fā)現(xiàn)ExecutionResult這樣的屬性舷胜,那開發(fā)人員便知道這個Command執(zhí)行完不會返回執(zhí)行結(jié)果娩践。
到目前為止,我們所討論的Command都是同步執(zhí)行的烹骨,如果Command被設(shè)計為異步執(zhí)行翻伺,那本文所討論的內(nèi)容便可以直接忽略。
如果系統(tǒng)的性能可以滿足需求沮焕,同步Command無疑是最好的吨岭。

CQRS架構(gòu)的優(yōu)點

  • CQ兩端架構(gòu)分離、相互不受束縛峦树,各自獨立設(shè)計辣辫、擴(kuò)展
  • C端通常結(jié)合DDD,解決復(fù)雜的業(yè)務(wù)邏輯魁巩;Q端輕量級查詢急灭,多種不同的查詢視圖通過訂閱事件來更新
  • C端通過分布式消息隊列水平擴(kuò)展,天然支持削峰
  • EDA架構(gòu)谷遂,整個系統(tǒng)各個部分松耦合葬馋,可擴(kuò)展性好
  • 架構(gòu)層面做到無并發(fā),實現(xiàn)Command的高吞吐
  • 技術(shù)架構(gòu)和業(yè)務(wù)代碼完全分離肾扰,程序員不用關(guān)心技術(shù)問題
  • 更方便的分工合作

CQRS架構(gòu)的缺點

  • 不是強(qiáng)一致性畴嘶,而是面向最終一致性
  • 強(qiáng)依賴高性能可靠的分布式消息隊列
  • 必須有強(qiáng)大可靠的CQRS框架,從頭做起成本高集晚、風(fēng)險大
  • 必須結(jié)合Event Sourcing模式窗悯,否則CQ分離意義不大
  • Event Sourcing模式的缺點
  • 一些CQRS的最佳原則提高了開發(fā)人員的門檻
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市甩恼,隨后出現(xiàn)的幾起案子蟀瞧,更是在濱河造成了極大的恐慌沉颂,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,548評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件悦污,死亡現(xiàn)場離奇詭異铸屉,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)切端,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,497評論 3 399
  • 文/潘曉璐 我一進(jìn)店門彻坛,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人踏枣,你說我怎么就攤上這事昌屉。” “怎么了茵瀑?”我有些...
    開封第一講書人閱讀 167,990評論 0 360
  • 文/不壞的土叔 我叫張陵间驮,是天一觀的道長。 經(jīng)常有香客問我马昨,道長竞帽,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,618評論 1 296
  • 正文 為了忘掉前任鸿捧,我火速辦了婚禮屹篓,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘匙奴。我一直安慰自己堆巧,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 68,618評論 6 397
  • 文/花漫 我一把揭開白布泼菌。 她就那樣靜靜地躺著谍肤,像睡著了一般。 火紅的嫁衣襯著肌膚如雪灶轰。 梳的紋絲不亂的頭發(fā)上谣沸,一...
    開封第一講書人閱讀 52,246評論 1 308
  • 那天,我揣著相機(jī)與錄音笋颤,去河邊找鬼乳附。 笑死,一個胖子當(dāng)著我的面吹牛伴澄,可吹牛的內(nèi)容都是我干的赋除。 我是一名探鬼主播,決...
    沈念sama閱讀 40,819評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼非凌,長吁一口氣:“原來是場噩夢啊……” “哼举农!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起敞嗡,我...
    開封第一講書人閱讀 39,725評論 0 276
  • 序言:老撾萬榮一對情侶失蹤颁糟,失蹤者是張志新(化名)和其女友劉穎航背,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體棱貌,經(jīng)...
    沈念sama閱讀 46,268評論 1 320
  • 正文 獨居荒郊野嶺守林人離奇死亡玖媚,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,356評論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了婚脱。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片今魔。...
    茶點故事閱讀 40,488評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖障贸,靈堂內(nèi)的尸體忽然破棺而出错森,到底是詐尸還是另有隱情,我是刑警寧澤篮洁,帶...
    沈念sama閱讀 36,181評論 5 350
  • 正文 年R本政府宣布涩维,位于F島的核電站,受9級特大地震影響嘀粱,放射性物質(zhì)發(fā)生泄漏激挪。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,862評論 3 333
  • 文/蒙蒙 一锋叨、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧宛篇,春花似錦娃磺、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,331評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至吆倦,卻和暖如春听诸,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背蚕泽。 一陣腳步聲響...
    開封第一講書人閱讀 33,445評論 1 272
  • 我被黑心中介騙來泰國打工晌梨, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人须妻。 一個月前我還...
    沈念sama閱讀 48,897評論 3 376
  • 正文 我出身青樓仔蝌,卻偏偏與公主長得像,于是被迫代替她去往敵國和親荒吏。 傳聞我的和親對象是個殘疾皇子敛惊,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,500評論 2 359

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

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn)绰更,斷路器瞧挤,智...
    卡卡羅2017閱讀 134,696評論 18 139
  • Spring Boot 參考指南 介紹 轉(zhuǎn)載自:https://www.gitbook.com/book/qbgb...
    毛宇鵬閱讀 46,846評論 6 342
  • 1. Java基礎(chǔ)部分 基礎(chǔ)部分的順序:基本語法锡宋,類相關(guān)的語法,內(nèi)部類的語法特恬,繼承相關(guān)的語法执俩,異常的語法,線程的語...
    子非魚_t_閱讀 31,661評論 18 399
  • 那天晚上接孩子鸵鸥,我看到了它奠滑,一只小貓,肚子是白的妒穴,身上的黑毛像是披著的一件黑外套宋税,在冷颼颼的晚風(fēng)下面,外套好像隨時...
    新意燕兒閱讀 196評論 2 0
  • 0很久之后讼油,江湖都會傳說杰赛,有一伙人,某個夜晚矮台,進(jìn)行一次成長總結(jié)的時候聊到天打雷劈乏屯。 14月28號的時候。有些感觸瘦赫。...
    oulan閱讀 200評論 0 0