摘要:?在 2016 年 11 月份的《技術(shù)雷達(dá)》中读虏,ThoughtWorks 給予了微服務(wù)很高的評價嘱支。同時指蚁,也有越來越多的組織將實施微服務(wù)作為架構(gòu)演進(jìn)的一個必選方向菱父。只不過在擁有眾多遺留系統(tǒng)的組織內(nèi)颈娜,將曾經(jīng)的單體系統(tǒng)拆分為微服務(wù)并不是一件容易的事情剑逃。
Credit:?Justin Kenneth Rowley. You can find the original photo at?flickr.
The microservices style of architecture highlights rising abstractions in the developer world because of containerization and the emphasis on low coupling, offering a high level of operational isolation. Developers can think of a container as a self-contained process and the PaaS as the common deployment target, using the microservices architecture as the common style. Decoupling the architecture allows the same for teams, cutting down on coordination cost among silos. Its attractiveness to both developers and DevOps has made this the de facto standard for new development in many organizations.
在 2016 年 11 月份的《技術(shù)雷達(dá)》中,ThoughtWorks 給予了微服務(wù)很高的評價官辽。同時蛹磺,也有越來越多的組織將實施微服務(wù)作為架構(gòu)演進(jìn)的一個必選方向。只不過在擁有眾多遺留系統(tǒng)的組織內(nèi)同仆,將曾經(jīng)的單體系統(tǒng)拆分為微服務(wù)并不是一件容易的事情萤捆。本文將從對遺留系統(tǒng)進(jìn)行微服務(wù)改造的原則要求出發(fā),探討如何使用 Dubbo 框架實現(xiàn)單體系統(tǒng)向微服務(wù)的遷移俗批。
一俗或、原則要求
想要對標(biāo)準(zhǔn)三層架構(gòu)的單體系統(tǒng)進(jìn)行微服務(wù)改造——簡言之——就是將曾經(jīng)單一進(jìn)程內(nèi)服務(wù)之間的本地調(diào)用改造為跨進(jìn)程的分布式調(diào)用。這雖然不是微服務(wù)改造的全部內(nèi)容岁忘,但卻直接決定了改造前后的系統(tǒng)能否保持相同的業(yè)務(wù)能力辛慰,以及改造成本的多少。
1.1 適合的框架
在微服務(wù)領(lǐng)域干像,雖然技術(shù)棧眾多帅腌,但無非 RPC 與 RESTful 兩個流派,這其中最具影響力的代表當(dāng)屬?Dubbo?與?Spring Cloud?了 蝠筑。他們擁有相似的能力狞膘,卻有著截然不同的實現(xiàn)方式——本文并不是想要對微服務(wù)框架的選型過程進(jìn)行深入剖析,也不想對這兩種框架的孰優(yōu)孰劣進(jìn)行全面比較——本章所提到的全部這些原則要求都是超越具體實現(xiàn)的什乙,其之于任何微服務(wù)框架都應(yīng)該是適用的挽封。讀者朋友們大可以把本文中的 Dubbo 全部替換為 Spring Cloud,而并不會對最終結(jié)果造成任何影響臣镣,唯一需要改變的僅僅是實現(xiàn)的細(xì)節(jié)過程而已辅愿。因此,無論最后抉擇如何忆某,都是無所謂對錯的点待,關(guān)鍵在于:要選擇符合組織當(dāng)下現(xiàn)狀的最適合的那一個。
1.2 方便的將服務(wù)暴露為遠(yuǎn)程接口
單體系統(tǒng)弃舒,服務(wù)之間的調(diào)用是在同一個進(jìn)程內(nèi)完成的癞埠;而微服務(wù),是將獨立的業(yè)務(wù)模塊拆分到不同的應(yīng)用系統(tǒng)中聋呢,每個應(yīng)用系統(tǒng)可以作為獨立的進(jìn)程來部署和運行苗踪。因此進(jìn)行微服務(wù)改造,就需要將進(jìn)程內(nèi)方法調(diào)用改造為進(jìn)程間通信削锰。進(jìn)程間通信的實現(xiàn)方式有很多種通铲,但顯然基于網(wǎng)絡(luò)調(diào)用的方式是最通用且易于實現(xiàn)的。那么能否方便的將本地服務(wù)暴露為網(wǎng)絡(luò)服務(wù)器贩,就決定了暴露過程能否被快速實施颅夺,同時暴露的過程越簡單則暴露后的接口與之前存在不一致性的風(fēng)險也就越低朋截。
1.3 方便的生成遠(yuǎn)程服務(wù)調(diào)用代理
當(dāng)服務(wù)被暴露為遠(yuǎn)程接口以后,進(jìn)程內(nèi)的本地實現(xiàn)將不復(fù)存在吧黄。簡化調(diào)用方的使用——為遠(yuǎn)程服務(wù)生成相應(yīng)的本地代理部服,將底層網(wǎng)絡(luò)交互細(xì)節(jié)進(jìn)行深層次的封裝——就顯得十分必要。另外遠(yuǎn)程服務(wù)代理在使用與功能上不應(yīng)該與原有本地實現(xiàn)有任何差別拗慨。
1.4 保持原有接口不變或向后兼容
在微服務(wù)改造過程中饲宿,要確保接口不變或向后兼容,這樣才不至于對調(diào)用方產(chǎn)生巨大影響胆描。在實際操作過程中瘫想,我們有可能僅僅可以掌控被改造的系統(tǒng),而無法訪問或修改調(diào)用方系統(tǒng)昌讲。倘若接口發(fā)生重大變化国夜,調(diào)用方系統(tǒng)的維護(hù)人員會難以接受:這會對他們的工作產(chǎn)生不可預(yù)估的風(fēng)險和沖擊,還會因為適配新接口而產(chǎn)生額外的工作量短绸。
1.5 保持原有的依賴注入關(guān)系不變
基于 Spring 開發(fā)的遺留系統(tǒng)车吹,服務(wù)之間通常是以依賴注入的方式彼此關(guān)聯(lián)的。進(jìn)行微服務(wù)改造后醋闭,原本注入的服務(wù)實現(xiàn)變成了本地代理窄驹,為了盡量減少代碼變更,最好能夠自動將注入的實現(xiàn)類切換為本地代理证逻。
1.6 保持原有代碼的作用或副作用效果不變
這一點看上去有些復(fù)雜乐埠,但卻是必不可少的。改造后的系統(tǒng)跟原有系統(tǒng)保持相同的業(yè)務(wù)能力囚企,當(dāng)且僅當(dāng)改造后的代碼與原有代碼保持相同的作用甚至是副作用丈咐。這里要額外提及的是副作用。我們在改造過程中可以很好的關(guān)注一般作用效果龙宏,卻往往會忽視副作用的影響棵逊。舉個例子,Java 內(nèi)部進(jìn)行方法調(diào)用的時候參數(shù)是以引用的方式傳遞的银酗,這意味著在方法體中可以修改參數(shù)里的值辆影,并將修改后的結(jié)果“返回”給被調(diào)用方∈蛱兀看下面的例子會更容易理解:
public void innerMethod(Map map) {
map.put("key", "new");
}
public void outerMethod() {
Map map = new HashMap<>();
map.put("key", "old");
System.out.println(map); // {key=old}
this.innerMethod(map);
System.out.println(map); // {key=new}
}
這段代碼在同一個進(jìn)程中運行是沒有問題的蛙讥,因為兩個方法共享同一片內(nèi)存空間,innerMethod對map的修改可以直接反映到outerMethod方法中衅澈。但是在微服務(wù)場景下事實就并非如此了键菱,此時innerMethod和outerMethod運行在兩個獨立的進(jìn)程中谬墙,進(jìn)程間的內(nèi)存相互隔離今布,innerMethod修改的內(nèi)容必須要主動回傳才能被outerMethod接收到经备,僅僅修改參數(shù)里的值是無法達(dá)到回傳數(shù)據(jù)的目的的。
此處副作用的概念是指在方法體中對傳入?yún)?shù)的內(nèi)容進(jìn)行了修改部默,并由此對外部上下文產(chǎn)生了可察覺的影響侵蒙。顯然副作用是不友好且應(yīng)該被避免的,但由于是遺留系統(tǒng)傅蹂,我們不能保證其中不會存在諸如此類寫法的代碼纷闺,所以我們還是需要在微服務(wù)改造過程中,對副作用的影響效果進(jìn)行保持份蝴,以獲得更好的兼容性犁功。
1.7 盡量少改動(最好不改動)遺留系統(tǒng)的內(nèi)部代碼
多數(shù)情況下,并非所有遺留系統(tǒng)的代碼都是可以被平滑改造的:比如婚夫,上面提到的方法具有副作用的情況浸卦,以及傳入和傳出參數(shù)為不可序列化對象(未實現(xiàn)?Serializable?接口)的情況等。我們雖然不能百分之百保證不對遺留系統(tǒng)的代碼進(jìn)行修改案糙,但至少應(yīng)該保證這些改動被控制在最小范圍內(nèi)限嫌,盡量采取變通的方式——例如添加而不是修改代碼——這種僅添加的改造方式至少可以保證代碼是向后兼容的。
1.8 良好的容錯能力
不同于進(jìn)程內(nèi)調(diào)用时捌,跨進(jìn)程的網(wǎng)絡(luò)通信可靠性不高怒医,可能由于各種原因而失敗。因此在進(jìn)行微服務(wù)改造的時候奢讨,遠(yuǎn)程方法調(diào)用需要更多考慮容錯能力稚叹。當(dāng)遠(yuǎn)程方法調(diào)用失敗的時候,可以進(jìn)行重試拿诸、恢復(fù)或者降級入录,否則不加處理的失敗會沿著調(diào)用鏈向上傳播(冒泡),從而導(dǎo)致整個系統(tǒng)的級聯(lián)失敗佳镜。
1.9 改造結(jié)果可插拔
針對遺留系統(tǒng)的微服務(wù)改造不可能保證一次性成功僚稿,需要不斷嘗試和改進(jìn),這就要求在一段時間內(nèi)原有代碼與改造后的代碼并存蟀伸,且可以通過一些簡單的配置讓系統(tǒng)在原有模式和微服務(wù)模式之間進(jìn)行無縫切換蚀同。優(yōu)先嘗試微服務(wù)模式,一旦出現(xiàn)問題可以快速切換回原有模式(手動或自動)啊掏,循序漸進(jìn)蠢络,直到微服務(wù)模式變得穩(wěn)定。
1.10 更多
當(dāng)然微服務(wù)改造的要求遠(yuǎn)不止上面提到的這些點迟蜜,還應(yīng)該包括諸如:配置管理刹孔、服務(wù)注冊與發(fā)現(xiàn)、負(fù)載均衡娜睛、網(wǎng)關(guān)髓霞、限流降級卦睹、擴(kuò)縮容、監(jiān)控和分布式事務(wù)等方库,然而這些需求大部分是要在微服務(wù)系統(tǒng)已經(jīng)升級改造完畢结序,復(fù)雜度不斷增加,流量上升到一定程度之后才會遇到和需要的纵潦,因此并不是本文關(guān)注的重點徐鹤。但這并不意味著這些內(nèi)容就不重要,沒有他們微服務(wù)系統(tǒng)同樣也是無法正常邀层、平穩(wěn)返敬、高速運行的。
二寥院、模擬一個單體系統(tǒng)
2.1 系統(tǒng)概述
我們需要構(gòu)建一個具有三層架構(gòu)的單體系統(tǒng)來模擬遺留系統(tǒng)救赐,這是一個簡單的 Spring Boot 應(yīng)用,項目名叫做hello-dubbo只磷。本文涉及到的所有源代碼均可以到?Github?上查看和下載经磅。
首先,系統(tǒng)存在一個模型?User和對該模型進(jìn)行管理的 DAO钮追,并通過UserService向上層暴露訪問?User?模型的接口预厌;另外,還存在一個HelloService元媚,其調(diào)用UserService并返回一條問候信息轧叽;之后,由Controller對外暴露 RESTful 接口;最終再通過 Spring Boot 的Application整合成一個完整應(yīng)用。
2.2 模塊化拆分
通常來說衍慎,一個具有三層架構(gòu)的單體系統(tǒng),其 Controller网严、Service 和 DAO 是存在于一整個模塊內(nèi)的,如果要進(jìn)行微服務(wù)改造嗤无,就要先對這個整體進(jìn)行拆分震束。拆分的方法是以 Service 層為分界,將其分割為兩個子模塊:Service 層往上作為一個子模塊(稱為?hello-web)当犯,對外提供 RESTful 接口垢村;Service 層往下作為另外一個子模塊(稱為hello-core),包括 Service嚎卫、DAO 以及模型嘉栓。hello-core被hello-web依賴。當(dāng)然,為了更好的體現(xiàn)面向契約的編程精神侵佃,可以把?hello-core?再進(jìn)一步拆分:所有的接口和模型都獨立出來麻昼,形成hello-api,而hello-core依賴?hello-api趣钱。最終,拆分后的模塊關(guān)系如下:
hello-dubbo
|-- hello-web(包含 Application 和 Controller)
|-- hello-core(包含 Service 和 DAO 的實現(xiàn))
?? ?|-- hello-api(包含 Service 和 DAO 的接口以及模型)
2.3 核心代碼分析
2.3.1 User
public class User implements Serializable {
private String id;
private String name;
private Date createdTime;
public String getId() {
return this.id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
public Date getCreatedTime() {
return this.createdTime;
}
public void setCreatedTime(Date createdTime) {
this.createdTime = createdTime;
}
@Override
public String toString() {
}
}
User模型是一個標(biāo)準(zhǔn)的 POJO胚宦,實現(xiàn)了Serializable接口(因為模型數(shù)據(jù)要在網(wǎng)絡(luò)上傳輸首有,因此必須能夠支持序列化和反序列化)。為了方便控制臺輸出枢劝,這里覆蓋了默認(rèn)的?toString方法井联。
2.3.2 UserRepository
public interface UserRepository {
User getById(String id);
void create(User user);
}
UserRepository接口是訪問User模型的 DAO,為了簡單起見您旁,該接口只包含兩個方法:getById和create烙常。
2.3.3 InMemoryUserRepository
@Repository
public class InMemoryUserRepository implements UserRepository {
private static final Map STORE = new HashMap<>();
static {
}
@Override
public User getById(String id) {
return STORE.get(id);
}
@Override
public void create(User user) {
STORE.put(user.getId(), user);
}
}
InMemoryUserRepository是UserRepository接口的實現(xiàn)類。該類型使用一個Map對象STORE來存儲數(shù)據(jù)鹤盒,并通過靜態(tài)代碼塊向該對象內(nèi)添加了一個默認(rèn)用戶蚕脏。getById方法根據(jù)?id?參數(shù)從STORE中獲取用戶數(shù)據(jù),而create方法就是簡單將傳入的user對象存儲到STORE中侦锯。由于所有這些操作都只是在內(nèi)存中完成的驼鞭,因此該類型被叫做?InMemoryUserRepository。
2.3.4 UserService
public interface UserService {
User getById(String id);
void create(User user);
}
與UserRepository的方法一一對應(yīng)尺碰,向更上層暴露訪問接口挣棕。
2.3.5 DefaultUserService
@Service("userService")
public class DefaultUserService implements UserService {
private static final Logger LOGGER = LoggerFactory.getLogger(DefaultUserService.class);
@Autowired
private UserRepository userRepository;
@Override
public User getById(String id) {
}
@Override
public void create(User user) {
}
}
DefaultUserService是UserService接口的默認(rèn)實現(xiàn),并通過@Service注解聲明為一個服務(wù)亲桥,服務(wù) id 為userService(該 id 在后面會需要用到)洛心。該服務(wù)內(nèi)部注入了一個UserRepository類型的對象userRepository。getUserById方法根據(jù)?id?從userRepository中獲取數(shù)據(jù)题篷,而?createUser?方法則將傳入的?user?參數(shù)通過?userRepository.create方法存入词身,并在存入之前設(shè)置了該對象的創(chuàng)建時間。很顯然番枚,根據(jù) 1.6 節(jié)關(guān)于副作用的描述偿枕,為?user對象設(shè)置創(chuàng)建時間的操作就屬于具有副作用的操作,需要在微服務(wù)改造之后加以保留户辫。為了方便看到系統(tǒng)工作效果渐夸,這兩個方法里面都打印了日志。
2.3.6 HelloService
public interface HelloService {
String sayHello(String userId);
}
HelloService接口只提供一個方法sayHello渔欢,就是根據(jù)傳入的userId返回一條對該用戶的問候信息墓塌。
2.3.7 DefaultHelloService
@Service("helloService")
public class DefaultHelloService implements HelloService {
@Autowired
private UserService userService;
@Override
public String sayHello(String userId) {
}
}
DefaultHelloService是HelloService接口的默認(rèn)實現(xiàn),并通過?@Service?注解聲明為一個服務(wù),服務(wù) id 為helloService(同樣苫幢,該名稱在后面的改造過程中會被用到)访诱。該類型內(nèi)部注入了一個UserService類型的對象userService。sayHello方法根據(jù)userId參數(shù)通過?userService獲取用戶信息韩肝,并返回一條經(jīng)過格式化后的消息触菜。
2.3.8 Application
@SpringBootApplication
public class Application {
?public static void main(String[] args) throws Exception {
SpringApplication.run(Application.class, args);
}
}
Application類型是 Spring Boot 應(yīng)用的入口,詳細(xì)描述請參考 Spring Boot 的官方文檔哀峻,在此不詳細(xì)展開涡相。
2.3.9 Controller
@RestController
public class Controller {
@Autowired
private HelloService helloService;
@Autowired
private UserService userService;
@RequestMapping("/hello/{userId}")
public String sayHello(@PathVariable("userId") String userId) {
return this.helloService.sayHello(userId);
}
@RequestMapping(path = "/create", method = RequestMethod.POST)
public String createUser(@RequestParam("userId") String userId, @RequestParam("name") String name) {
}
}
Controller類型是一個標(biāo)準(zhǔn)的 Spring MVC Controller,在此不詳細(xì)展開討論剩蟀。僅僅需要說明的是這個類型注入了HelloService和UserService類型的對象催蝗,并在sayHello和?createUser?方法中調(diào)用了這兩個對象中的有關(guān)方法。
2.4 打包運行
hello-dubbo項目包含三個子模塊:hello-api育特、hello-core?和hello-web丙号,是用 Maven 來管理的。到目前為止所涉及到的 POM 文件都比較簡單缰冤,為了節(jié)約篇幅犬缨,就不在此一一列出了,感興趣的朋友可以到項目的 Github 倉庫上自行研究棉浸。
hello-dubbo項目的打包和運行都非常直接:
編譯遍尺、打包和安裝
在項目根目錄下執(zhí)行命令
$ mvn clean install
運行
在 hello-web 目錄下執(zhí)行命令
$ mvn spring-boot:run
測試結(jié)果如下,注意每次輸出括號里面的日期時間涮拗,它們都應(yīng)該是有值的乾戏。
再返回hello-web系統(tǒng)的控制臺,查看一下日志輸出三热,時間應(yīng)該與上面是一樣的鼓择。
三、動手改造
3.1 改造目標(biāo)
上一章就漾,我們已經(jīng)成功構(gòu)建了一個模擬系統(tǒng)呐能,該系統(tǒng)是一個單體系統(tǒng),對外提供了兩個 RESTful 接口抑堡。本章要達(dá)到的目標(biāo)是將該單體系統(tǒng)拆分為兩個獨立運行的微服務(wù)系統(tǒng)摆出。如 2.2 節(jié)所述,進(jìn)行模塊化拆分是實施微服務(wù)改造的重要一步首妖,因為在接下來的描述中會暗含一個約定:hello-web偎漫、hello-core和?hello-api這三個模塊與上一章中所設(shè)定的能力是相同的∮欣拢基于 1.7 節(jié)所提到的“盡量少改動(最好不改動)遺留系統(tǒng)的內(nèi)部代碼”的改造要求象踊,這三個模塊中的代碼是不會被大面積修改的温亲,只會有些許調(diào)整,以適應(yīng)新的微服務(wù)環(huán)境杯矩。
具體將要實現(xiàn)的目標(biāo)效果如下:
第一個微服務(wù)系統(tǒng):
hello-web(包含 Application 和 Controller)
|-- hello-service-reference(包含 Dubbo 有關(guān)服務(wù)引用的配置)
|-- hello-api(包含 Service 和 DAO 的接口以及模型)
第二個微服務(wù)系統(tǒng):
hello-service-provider(包含 Dubbo 有關(guān)服務(wù)暴露的配置)
|-- hello-core(包含 Service 和 DAO 的實現(xiàn))
|-- hello-api(包含 Service 和 DAO 的接口以及模型)
hello-web與原來一樣栈虚,是一個面向最終用戶提供 Web 服務(wù)的終端系統(tǒng),其只包含 Application史隆、Controller魂务、Service 接口、 DAO 接口以及模型泌射,因此它本身是不具備任何業(yè)務(wù)能力的粘姜,必須通過依賴?hello-service-reference?模塊來遠(yuǎn)程調(diào)用hello-service-provider系統(tǒng)才能完成業(yè)務(wù)。而hello-service-provider?系統(tǒng)則需要暴露可供?hello-service-reference模塊調(diào)用的遠(yuǎn)程接口魄幕,并實現(xiàn) Service 及 DAO 接口定義的具體業(yè)務(wù)邏輯相艇。
本章節(jié)就是要重點介紹?hello-service-provider?和hello-service-reference模塊是如何構(gòu)建的颖杏,以及它們在微服務(wù)改造過程中所起到的作用纯陨。
3.2 暴露遠(yuǎn)程服務(wù)
Spring Boot 和 Dubbo 的結(jié)合使用可以引入諸如spring-boot-starter-dubbo這樣的起始包,使用起來會更加方便留储。但是考慮到項目的單純性和通用性翼抠,本文仍然延用 Spring 經(jīng)典的方式進(jìn)行配置。
首先获讳,我們需要創(chuàng)建一個新的模塊阴颖,叫做?hello-service-provider,這個模塊的作用是用來暴露遠(yuǎn)程服務(wù)接口的丐膝。依托于 Dubbo 強(qiáng)大的服務(wù)暴露及整合能力量愧,該模塊不用編寫任何代碼,僅需添加一些配置即可完成帅矗。
注:有關(guān) Dubbo 的具體使用和配置說明并不是本文討論的重點偎肃,請參考官方文檔。
3.2.1 添加 dubbo-services.xml 文件
dubbo-services.xml
?配置是該模塊的關(guān)鍵浑此,Dubbo 就是根據(jù)這個文件累颂,自動暴露遠(yuǎn)程服務(wù)的。這是一個標(biāo)準(zhǔn) Spring 風(fēng)格的配置文件凛俱,引入了 Dubbo 命名空間紊馏,需要將其擺放在?
src/main/resources/META-INF/spring
?目錄下,這樣 Maven 在打包的時候會自動將其添加到 classpath蒲犬。
3.2.2 添加 POM 文件
有關(guān) Maven 的使用與配置也不是本文關(guān)注的重點朱监,但是這個模塊用到了一些 Maven 插件,在此對這些插件的功能和作用進(jìn)行一下描述原叮。
3.2.3添加 assembly.xml 文件
Assembly 插件的主要功能是對項目重新打包赌朋,以便自定義打包方式和內(nèi)容凰狞。對本項目而言,需要生成一個壓縮包沛慢,里面包含所有運行該服務(wù)所需要的 jar 包赡若、配置文件和啟動腳本等。Assembly 插件需要assembly.xml?
文件來描述具體的打包過程团甲,該文件需要擺放在src/main/assembly目錄下逾冬。有關(guān)assembly.xml?文件的具體配置方法,請參考官方文檔躺苦。
3.2.4 添加 logback.xml 文件
由于在 POM 文件中指定了使用 logback 作為日志輸出組件身腻,因此還需要在logback.xml?
文件中對其進(jìn)行配置。該文件需要擺放在src/main/resources?目錄下匹厘,有關(guān)該配置文件的具體內(nèi)容請參見代碼倉庫嘀趟,有關(guān)配置的詳細(xì)解釋,請參考官方文檔愈诚。
3.2.5 打包
由于已經(jīng)在 POM 文件中定義了打包的相關(guān)配置她按,因此直接在?hello-service-provider目錄下運行以下命令即可:
$ mvn clean package
成功執(zhí)行以后,會在其 target 目錄下生成一個名為
?hello-service-provider-0.1.0-SNAPSHOT-assembly.tar.gz
?的壓縮包炕柔,里面的內(nèi)容如圖所示:
3.2.6 運行
如此配置完成以后酌泰,就可以使用如下命令來啟動服務(wù):
$ MAVEN_OPTS="-Djava.net.preferIPv4Stack=true" mvn exec:java
注:在 macOS 系統(tǒng)里,使用 multicast 機(jī)制進(jìn)行服務(wù)注冊與發(fā)現(xiàn)匕累,需要添加-Djava.net.preferIPv4Stack=true參數(shù)陵刹,否則會拋出異常。
可以使用如下命令來判斷服務(wù)是否正常運行:
$ netstat -antl | grep 20880
如果有類似如下的信息輸出欢嘿,則說明運行正常衰琐。
如果是在正式環(huán)境運行,就需要將上一步生成的壓縮包解壓炼蹦,然后運行bin目錄下的相應(yīng)腳本即可羡宙。
3.2.7 總結(jié)
使用這種方式來暴露遠(yuǎn)程服務(wù)具有如下一些優(yōu)勢:
使用 Dubbo 進(jìn)行遠(yuǎn)程服務(wù)暴露,無需關(guān)注底層實現(xiàn)細(xì)節(jié)
對原系統(tǒng)沒有任何入侵框弛,已有系統(tǒng)可以繼續(xù)按照原來的方式啟動和運行
暴露過程可插拔
Dubbo 服務(wù)與原有服務(wù)在開發(fā)期和運行期均可以共存
無需編寫任何代碼
3.3 引用遠(yuǎn)程服務(wù)
3.3.1 添加服務(wù)引用
與hello-service-provider模塊的處理方式相同辛辨,為了不侵入原有系統(tǒng),我們創(chuàng)建另外一個模塊瑟枫,叫做?hello-service-reference斗搞。這個模塊只有一個配置文件dubbo-references.xml放置在src/main/resources/META-INF/spring/?目錄下。文件的內(nèi)容非常簡單明了:
但不同于?hello-service-provider模塊的一點在于慷妙,該模塊只需要打包成一個 jar 即可僻焚,POM 文件內(nèi)容如下:
總結(jié)一下,我們曾經(jīng)的遺留系統(tǒng)分為三個模塊?hello-web,?hello-core和?hello-api膝擂。經(jīng)過微服務(wù)化處理以后虑啤,hello-core和hello-api被剝離了出去隙弛,加上hello-service-provider?模塊,形成了一個可以獨立運行的?hello-service-provider系統(tǒng)狞山,因此需要打包成一個完整的應(yīng)用全闷;而?hello-web要想調(diào)用hello-core提供的服務(wù),就不能再直接依賴?hello-core模塊了萍启,而是需要依賴我們這里創(chuàng)建的?hello-service-reference模塊总珠,因此hello-service-reference是作為一個依賴庫出現(xiàn)的,其目的就是遠(yuǎn)程調(diào)用hello-service-provider暴露出來的服務(wù)勘纯,并提供本地代理局服。
這時?hello-web模塊的依賴關(guān)系就發(fā)生了變化:原來?hello-web?模塊直接依賴hello-core,再通過?hellocore間接依賴?hello-api驳遵,而現(xiàn)在我們需要將其改變?yōu)橹苯右蕾噃ello-service-reference模塊淫奔,再通過hello-service-reference?模塊間接依賴?hello-api。改造前后的依賴關(guān)系分別為:
3.3.2 啟動服務(wù)
因為是測試環(huán)境堤结,只需要執(zhí)行以下命令即可唆迁,但在進(jìn)行本操作之前,需要先啟動hello-service-provider服務(wù)霍殴。
$ MAVEN_OPTS="-Djava.net.preferIPv4Stack=true" mvn spring-boot:run
Oops媒惕!系統(tǒng)并不能像期望的那樣正常運行系吩,會拋出如下異常:
意思是說?net.tangrui.demo.dubbo.hello.web.Controller?這個類的helloService字段需要一個類型為net.tangrui.demo.dubbo.hello.service.HelloService?的 Bean来庭,但是沒有找到。相關(guān)代碼片段如下:
@RestController
Public class Controller {
@Autowired
private HelloService helloService;
@Autowired
private UserService userService;
...
}
顯然穿挨,helloService和userService都是無法注入的月弛,這是為什么呢?
原因自然跟我們修改hello-web這個模塊的依賴關(guān)系有關(guān)科盛。原本hello-web是依賴于hello-core的帽衙,hello-core里面聲明了HelloService和?UserService這兩個服務(wù)(通過?@Service注解),然后Controller在?@Autowired的時候就可以自動綁定了贞绵。但是厉萝,現(xiàn)在我們將?hello-core替換成了?hello-service-reference,在?hello-service-reference的配置文件中聲明了兩個對遠(yuǎn)程服務(wù)的引用榨崩,按道理來說這個注入應(yīng)該是可以生效的谴垫,但顯然實際情況并非如此。
仔細(xì)思考不難發(fā)現(xiàn)母蛛,我們在執(zhí)行?mvn exec:java?命令啟動?hello-service-provider模塊的時候指定了啟動com.alibaba.dubbo.container.Main?類型翩剪,然后才會開始啟動并加載 Dubbo 的有關(guān)配置,這一點從日志中可以得到證實(日志里面會打印出來很多帶有?[DUBBO]標(biāo)簽的內(nèi)容)彩郊,顯然在這次運行中前弯,我們并沒有看到類似這樣的日志蚪缀,說明 Dubbo 在這里沒有被正確啟動。歸根結(jié)底還是 Spring Boot 的原因恕出,即 Spring Boot 需要一些配置才能夠正確加載和啟動 Dubbo询枚。
讓 Spring Boot 支持 Dubbo 有很多種方法,比如前面提到的?spring-boot-starter-dubbo?起始包浙巫,但這里同樣為了簡單和通用哩盲,我們依舊采用經(jīng)典的方式來解決。
繼續(xù)思考狈醉,該模塊沒有成功啟動 Dubbo廉油,僅僅是因為添加了對hello-service-reference的引用,而hello-service-reference模塊就只有一個文件dubbo-references.xml苗傅,這就說明 Spring Boot 并沒有加載到這個文件抒线。順著這個思路,只需要讓 Spring Boot 能夠成功加載這個文件渣慕,問題就可以了嘶炭。Spring Boot 也確實提供了這樣的能力,只可惜無法完全做到代碼無侵入逊桦,只能說這些改動是可以被接受的眨猎。修改方式是替換Application中的注解(至于為什么要修改成這樣的結(jié)果,超出了本文的討論范圍强经,請自行 Google)睡陪。
@Configuration
@EnableAutoConfiguration
@ComponentScan
@ImportResource("classpath:META-INF/spring/dubbo-references.xml")
public class Application {
public static void main(String[] args) throws Exception {SpringApplication.run(Application.class, args);
}
}
這里的主要改動,是將一個?@SpringBootApplication?注解替換為@Configuration匿情、@EnableAutoConfiguration兰迫、@ComponentScan?和@ImportResource?四個注解。不難看出炬称,最后一個?@ImportResource就是我們需要的汁果。
這時再重新嘗試啟動,就一切正常了玲躯。
但是据德,我們?nèi)绾悟炞C結(jié)果確實是從hello-service-provider服務(wù)過來的呢?這時就需要用到?DefaultUserService里面的那幾行日志輸出了跷车,回到?hello-service-provider服務(wù)的控制臺棘利,能夠看到類似這樣的輸出:
如此便可以確信系統(tǒng)的拆分是被成功實現(xiàn)了。再試試創(chuàng)建用戶的接口:
$ curl -X POST 'http://127.0.0.1:8080/create?userId=huckleberry&name=Huckleberry%20Finn'
等等姓赤,什么赡译!括號里面的創(chuàng)建時間為什么是N/A,這說明createdTime字段根本沒有值不铆!
3.4 保持副作用效果
讓我們先來回顧一下 1.6 節(jié)所提到的副作用效果蝌焚。在DefaultUserService.create方法中裹唆,我們?yōu)閭魅氲膗ser參數(shù)設(shè)置了創(chuàng)建時間,這一操作就是我們要關(guān)注的具有副作用效果的操作只洒。
先說單體系統(tǒng)的情況许帐。單體系統(tǒng)是運行在一個 Java 虛擬機(jī)中的,所有對象共享一片內(nèi)存空間毕谴,彼此可以互相訪問成畦。系統(tǒng)在運行的時候,先是由Controller.create方法獲取用戶輸入涝开,將輸入的參數(shù)封裝為一個user對象循帐,再傳遞給UserService.create方法(具體是在調(diào)用DefaultUserService.create方法),這時user?對象的createdTime?字段就被設(shè)置了舀武。由于 Java 是以引用的方式來傳遞參數(shù)拄养,因此在create?方法中對?user?對象所做的變更,是能夠反映到調(diào)用方那里的——即Controller.create方法里面也是可以獲取到變更的银舱,所以返回給用戶的時候瘪匿,這個?createdTime就是存在的。
再說微服務(wù)系統(tǒng)的情況寻馏。此時系統(tǒng)是獨立運行在兩個虛擬機(jī)中的棋弥,彼此之間的內(nèi)存是相互隔離的。起始點同樣是hello-web系統(tǒng)的 Controller.create 方法:獲取用戶輸入诚欠,封裝 user 對象顽染。可是在調(diào)用 UserService.create 方法的時候聂薪,并不是直接調(diào)用DefaultUserService中的方法家乘,而是調(diào)用了一個具有相同接口的本地代理蝗羊,這個代理將?user對象序列化之后藏澳,通過網(wǎng)絡(luò)傳輸給了?hello-service-provider系統(tǒng)。該系統(tǒng)接收到數(shù)據(jù)以后耀找,先進(jìn)行反序列化翔悠,生成跟原來對象一模一樣的副本,再由UserService.create?方法進(jìn)行處理(這回調(diào)用的就是?DefaultUserService里面的實現(xiàn)了)野芒。至此蓄愁,這個被設(shè)置過createdTime的?user?對象副本是一直存在于hello-service-provider系統(tǒng)的內(nèi)存里面的,從來沒有被傳遞出去狞悲,自然是無法被?hello-web系統(tǒng)讀取到的撮抓,所以最終打印出來的結(jié)果,括號里面的內(nèi)容就是N/A了摇锋。記得我們有在DefaultUserService.create方法中輸出過日志丹拯,所以回到?hello-service-provider系統(tǒng)的控制臺站超,可以看到如下的日志信息,說明在這個系統(tǒng)里面?createdTime?字段確實是有值的乖酬。
那么該如何讓這個副作用效果也能夠被處于另外一個虛擬機(jī)中的hello-web?系統(tǒng)感知到呢死相,方法只有一種,就是將變更后的數(shù)據(jù)回傳咬像。
3.4.1 為方法添加返回值
這是最容易想到的一種實現(xiàn)方式算撮,簡單的說就是修改服務(wù)接口,將變更后的數(shù)據(jù)返回县昂。
首先肮柜,修改UserService接口的create方法,添加返回值:
public interface UserService {
...
// 為方法添加返回值
User create(User user);
}
然后倒彰,修改實現(xiàn)類中相應(yīng)的方法素挽,將變更后的user?對象返回:
@Service("userService")
public class DefaultUserService implements UserService {
...
@Override
public User create(User user) {
}
}
最后,修改調(diào)用方實現(xiàn)狸驳,接收返回值:
@RestController
public class Controller {
...
@RequestMapping(path = "/create", method = RequestMethod.POST)
public String createUser(@RequestParam("userId") String userId, @RequestParam("name") String name) {
}
}
編譯预明、運行并測試(如下圖),正如我們所期望的耙箍,括號中的創(chuàng)建時間又回來了撰糠。其工作原理與本節(jié)開始時所描述的是一樣的,只是方向相反而已辩昆。在此不再詳細(xì)展開阅酪,留給大家自行思考。
這種修改方式有如下一些優(yōu)缺點:
方法簡單汁针,容易理解
改變了系統(tǒng)接口术辐,且改變后的接口與原有接口不兼容(違背了 1.4 節(jié)關(guān)于“保持原有接口不變或向后兼容”原則的要求)
由此也不可避免的造成了對遺留系統(tǒng)內(nèi)部代碼的修改(違背了 1.7 節(jié)關(guān)于“盡量少改動(最好不改動)遺留系統(tǒng)的內(nèi)部代碼”原則的要求)
修改方式不可插拔(違背了 1.9 節(jié)“改造結(jié)果可插拔”原則的要求)
由此可見,這種改法雖然簡單施无,卻是利大于弊的辉词,除非我們能夠完全掌控整個系統(tǒng),否則這種修改方式的風(fēng)險會隨著系統(tǒng)復(fù)雜性的增加而急劇上升猾骡。
3.4.2 添加一個新方法
如果不能做到不改變接口瑞躺,那我們至少要做到改變后的接口與原有接口向后兼容。保證向后兼容性的一種解決辦法兴想,就是不改變原有方法幢哨,而是添加一個新的方法。過程如下:
首先嫂便,為UserService接口添加一個新的方法__rpc_create捞镰。這個方法名雖然看起來有些奇怪,但卻有兩點好處:第一、不會和已有方法重名岸售,因為 Java 命名規(guī)范不建議使用這樣的標(biāo)識符來為方法命名几迄;第二、在原有方法前加上?__rpc_前綴冰评,能夠做到與原有方法對應(yīng)映胁,便于閱讀和理解。示例如下:
public interface UserService {
...
// 保持原有方法不變
void create(User user);
// 添加一個方法甲雅,新方法需要有返回值
User __rpc_create(User user);
}
然后解孙,在實現(xiàn)類中實現(xiàn)這個新方法:
@Service("userService")
public class DefaultUserService implements UserService {
...
// 保持原有方法實現(xiàn)不變
@Override
public void create(User user) {
}
// 添加新方法的實現(xiàn)
@Override
public User __rpc_create(User user) {
}
}
有一點需要展開解釋:在__rpc_create?方法中,因為user?參數(shù)是以引用的方式傳遞給?create方法的抛人,因此create?方法對參數(shù)所做的修改是能夠被?__rpc_create方法獲取到的弛姜。這以后就與前面回傳的邏輯是相同的了。
第三妖枚,在服務(wù)引用端添加本地存根(有關(guān)本地存根的概念及用法廷臼,請參考官方文檔)。
需要在hello-service-reference模塊中添加一個類UserServiceStub绝页,內(nèi)容如下:
public class UserServiceStub implements UserService {
private UserService userService;
public UserServiceStub(UserService userService) {
this.userService = userService;
}
@Override
public User getById(String id) {
return this.userService.getById(id);
}
@Override
public void create(User user) {
User newUser = this.__rpc_create(user);
?user.setCreatedTime(newUser.getCreatedTime());
}
@Override
public User __rpc_create(User user) {
return this.userService.__rpc_create(user);
}
}
該類型即為本地存根荠商。簡單來說,就是在調(diào)用方調(diào)用本地代理的方法之前续誉,會先去調(diào)用本地存根中相應(yīng)的方法莱没,因此本地存根與服務(wù)提供方和服務(wù)引用方需要實現(xiàn)同樣的接口。本地存根中的構(gòu)造函數(shù)是必須的酷鸦,且方法簽名也是被約定好的——需要傳入本地代理作為參數(shù)饰躲。其中g(shù)etById和?__rpc_create?方法都是直接調(diào)用了本地代理中的方法,不必過多關(guān)注臼隔,重點來說說create?方法嘹裂。首先,create?調(diào)用了本地存根中的?__rpc_create方法摔握,這個方法透過本地代理訪問到了服務(wù)提供方的相應(yīng)方法寄狼,并成功接收了返回值?newUser,這個返回值是包含修改后的?createdTime?字段的,于是我們要做的事情就是從newUser?對象里面獲取到?createdTime字段的值贫橙,并設(shè)置給user參數(shù),以達(dá)到產(chǎn)生副作用的效果。此時user參數(shù)會帶著新設(shè)置的createdTime?的值映屋,將其“傳遞”給create?方法的調(diào)用方。
最后饮焦,在?dubbo-references.xml?文件中修改一處配置讥蟆,以啟用該本地存根:
interface="net.tangrui.demo.dubbo.hello.service.UserService"
version="1.0"
stub="net.tangrui.demo.dubbo.hello.service.stub.UserServiceStub" />
鑒于本地存根的工作機(jī)制,我們是不需要修改調(diào)用方hello-web模塊中的任何代碼及配置的雀彼。編譯壤蚜、運行并測試即寡,同樣可以達(dá)到我們想要的效果。
這種實現(xiàn)方式會比第一種方式改進(jìn)不少袜刷,但也有致命弱點:
保持了接口的向后兼容性
引入本地存根聪富,無需修改調(diào)用方代碼
通過配置可以實現(xiàn)改造結(jié)果的可插拔
實現(xiàn)復(fù)雜,尤其是本地存根的實現(xiàn)著蟹,如果遺留系統(tǒng)的代碼對傳入?yún)?shù)里的內(nèi)容進(jìn)行了無節(jié)制的修改的話墩蔓,那么重現(xiàn)該副作用效果是非常耗時且容易出錯的
難以理解
四、總結(jié)
至此萧豆,將遺留系統(tǒng)改造為微服務(wù)系統(tǒng)的任務(wù)就大功告成了奸披,而且基本上滿足了文章最開始提出來的十點改造原則與要求(此處應(yīng)給自己一些掌聲),不知道是否對大家有所幫助涮雷?雖然示例項目是為了敘述要求而量身定制的阵面,但文章中提到的種種理念與方法卻實實在在是從實踐中摸索和總結(jié)出來的——踩過的坑,遇到的問題洪鸭,解決的思路以及改造的難點等都一一呈現(xiàn)給了大家样刷。
微服務(wù)在當(dāng)下已經(jīng)不是什么新鮮的技術(shù)了,但歷史包袱依然是限制其發(fā)展的重要因素览爵,希望這篇文章能帶給大家一點啟發(fā)颂斜,在接下來的工作中更好的擁抱微服務(wù)帶來的變革。