国产一级a片免费看高清,亚洲熟女中文字幕在线视频,黄三级高清在线播放,免费黄色视频在线看

打開APP
userphoto
未登錄

開通VIP,暢享免費電子書等14項超值服

開通VIP
使用 Dubbo 對遺留單體系統(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ù)雷達》中,ThoughtWorks 給予了微服務(wù)很高的評價。同時,也有越來越多的組織將實施微服務(wù)作為架構(gòu)演進的一個必選方向。只不過在擁有眾多遺留系統(tǒng)的組織內(nèi),將曾經(jīng)的單體系統(tǒng)拆分為微服務(wù)并不是一件容易的事情。本文將從對遺留系統(tǒng)進行微服務(wù)改造的原則要求出發(fā),探討如何使用 Dubbo 框架實現(xiàn)單體系統(tǒng)向微服務(wù)的遷移。

一、原則要求

想要對標準三層架構(gòu)的單體系統(tǒng)進行微服務(wù)改造——簡言之——就是將曾經(jīng)單一進程內(nèi)服務(wù)之間的本地調(diào)用改造為跨進程的分布式調(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ù)框架的選型過程進行深入剖析,也不想對這兩種框架的孰優(yōu)孰劣進行全面比較——本章所提到的全部這些原則要求都是超越具體實現(xiàn)的,其之于任何微服務(wù)框架都應(yīng)該是適用的。讀者朋友們大可以把本文中的 Dubbo 全部替換為 Spring Cloud,而并不會對最終結(jié)果造成任何影響,唯一需要改變的僅僅是實現(xiàn)的細節(jié)過程而已。因此,無論最后抉擇如何,都是無所謂對錯的,關(guān)鍵在于:要選擇符合組織當(dāng)下現(xiàn)狀的最適合的那一個。

1.2 方便的將服務(wù)暴露為遠程接口

單體系統(tǒng),服務(wù)之間的調(diào)用是在同一個進程內(nèi)完成的;而微服務(wù),是將獨立的業(yè)務(wù)模塊拆分到不同的應(yīng)用系統(tǒng)中,每個應(yīng)用系統(tǒng)可以作為獨立的進程來部署和運行。因此進行微服務(wù)改造,就需要將進程內(nèi)方法調(diào)用改造為進程間通信。進程間通信的實現(xiàn)方式有很多種,但顯然基于網(wǎng)絡(luò)調(diào)用的方式是最通用且易于實現(xiàn)的。那么能否方便的將本地服務(wù)暴露為網(wǎng)絡(luò)服務(wù),就決定了暴露過程能否被快速實施,同時暴露的過程越簡單則暴露后的接口與之前存在不一致性的風(fēng)險也就越低。

1.3 方便的生成遠程服務(wù)調(diào)用代理

當(dāng)服務(wù)被暴露為遠程接口以后,進程內(nèi)的本地實現(xiàn)將不復(fù)存在。簡化調(diào)用方的使用——為遠程服務(wù)生成相應(yīng)的本地代理,將底層網(wǎng)絡(luò)交互細節(jié)進行深層次的封裝——就顯得十分必要。另外遠程服務(wù)代理在使用與功能上不應(yīng)該與原有本地實現(xiàn)有任何差別。

1.4 保持原有接口不變或向后兼容

在微服務(wù)改造過程中,要確保接口不變或向后兼容,這樣才不至于對調(diào)用方產(chǎn)生巨大影響。在實際操作過程中,我們有可能僅僅可以掌控被改造的系統(tǒng),而無法訪問或修改調(diào)用方系統(tǒng)。倘若接口發(fā)生重大變化,調(diào)用方系統(tǒng)的維護人員會難以接受:這會對他們的工作產(chǎn)生不可預(yù)估的風(fēng)險和沖擊,還會因為適配新接口而產(chǎn)生額外的工作量。

1.5 保持原有的依賴注入關(guān)系不變

基于 Spring 開發(fā)的遺留系統(tǒng),服務(wù)之間通常是以依賴注入的方式彼此關(guān)聯(liá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)部進行方法調(diào)用的時候參數(shù)是以引用的方式傳遞的,這意味著在方法體中可以修改參數(shù)里的值,并將修改后的結(jié)果“返回”給被調(diào)用方??聪旅娴睦訒菀桌斫猓?/p>


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}
}

這段代碼在同一個進程中運行是沒有問題的,因為兩個方法共享同一片內(nèi)存空間,innerMethod 對 map 的修改可以直接反映到 outerMethod 方法中。但是在微服務(wù)場景下事實就并非如此了,此時 innerMethod 和 outerMethod 運行在兩個獨立的進程中,進程間的內(nèi)存相互隔離,innerMethod修改的內(nèi)容必須要主動回傳才能被 outerMethod 接收到,僅僅修改參數(shù)里的值是無法達到回傳數(shù)據(jù)的目的的。

此處副作用的概念是指在方法體中對傳入?yún)?shù)的內(nèi)容進行了修改,并由此對外部上下文產(chǎn)生了可察覺的影響。顯然副作用是不友好且應(yīng)該被避免的,但由于是遺留系統(tǒng),我們不能保證其中不會存在諸如此類寫法的代碼,所以我們還是需要在微服務(wù)改造過程中,對副作用的影響效果進行保持,以獲得更好的兼容性。

1.7 盡量少改動(最好不改動)遺留系統(tǒng)的內(nèi)部代碼

多數(shù)情況下,并非所有遺留系統(tǒng)的代碼都是可以被平滑改造的:比如,上面提到的方法具有副作用的情況,以及傳入和傳出參數(shù)為不可序列化對象(未實現(xiàn) Serializable 接口)的情況等。我們雖然不能百分之百保證不對遺留系統(tǒng)的代碼進行修改,但至少應(yīng)該保證這些改動被控制在最小范圍內(nèi),盡量采取變通的方式——例如添加而不是修改代碼——這種僅添加的改造方式至少可以保證代碼是向后兼容的。

1.8 良好的容錯能力

不同于進程內(nèi)調(diào)用,跨進程的網(wǎng)絡(luò)通信可靠性不高,可能由于各種原因而失敗。因此在進行微服務(wù)改造的時候,遠程方法調(diào)用需要更多考慮容錯能力。當(dāng)遠程方法調(diào)用失敗的時候,可以進行重試、恢復(fù)或者降級,否則不加處理的失敗會沿著調(diào)用鏈向上傳播(冒泡),從而導(dǎo)致整個系統(tǒng)的級聯(lián)失敗。

1.9 改造結(jié)果可插拔

針對遺留系統(tǒng)的微服務(wù)改造不可能保證一次性成功,需要不斷嘗試和改進,這就要求在一段時間內(nèi)原有代碼與改造后的代碼并存,且可以通過一些簡單的配置讓系統(tǒng)在原有模式和微服務(wù)模式之間進行無縫切換。優(yōu)先嘗試微服務(wù)模式,一旦出現(xiàn)問題可以快速切換回原有模式(手動或自動),循序漸進,直到微服務(wù)模式變得穩(wěn)定。

1.10 更多

當(dāng)然微服務(wù)改造的要求遠不止上面提到的這些點,還應(yīng)該包括諸如:配置管理、服務(wù)注冊與發(fā)現(xiàn)、負載均衡、網(wǎng)關(guān)、限流降級、擴縮容、監(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 和對該模型進行管理的 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)的,如果要進行微服務(wù)改造,就要先對這個整體進行拆分。拆分的方法是以 Service 層為分界,將其分割為兩個子模塊:Service 層往上作為一個子模塊(稱為 hello-web),對外提供 RESTful 接口;Service 層往下作為另外一個子模塊(稱為 hello-core),包括 Service、DAO 以及模型。hello-core 被 hello-web 依賴。當(dāng)然,為了更好的體現(xiàn)面向契約的編程精神,可以把 hello-core 再進一步拆分:所有的接口和模型都獨立出來,形成 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() {

SimpleDateFormat sdf = new SimpleDateFormat('yyyy-MM-dd HH:mm:ss');if (this.getCreatedTime() != null) { return String.format('%s (%s)', this.getName(), sdf.format(this.getCreatedTime()));}return String.format('%s (N/A)', this.getName());

}
}

User 模型是一個標準的 POJO,實現(xiàn)了 Serializable 接口(因為模型數(shù)據(jù)要在網(wǎng)絡(luò)上傳輸,因此必須能夠支持序列化和反序列化)。為了方便控制臺輸出,這里覆蓋了默認的 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 {

User tom = new User();tom.setId('tom');tom.setName('Tom Sawyer');tom.setCreatedTime(new Date());STORE.put(tom.getId(), tom);

}

@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)添加了一個默認用戶。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) {

User user = this.userRepository.getById(id);LOGGER.info(user.toString());return user;

}

@Override
public void create(User user) {

user.setCreatedTime(new Date());this.userRepository.create(user);LOGGER.info(user.toString());

}
}

DefaultUserService 是 UserService 接口的默認實現(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) {

User user = this.userService.getById(userId);return String.format('Hello, %s.', user);

}
}

DefaultHelloService 是 HelloService 接口的默認實現(xiàn),并通過 @Service 注解聲明為一個服務(wù),服務(wù) id 為 helloService(同樣,該名稱在后面的改造過程中會被用到)。該類型內(nèi)部注入了一個 UserService 類型的對象 userServicesayHello 方法根據(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)用的入口,詳細描述請參考 Spring Boot 的官方文檔,在此不詳細展開。

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) {

User user = new User();user.setId(userId);user.setName(name);this.userService.createUser(user);return user.toString();

}
}

Controller 類型是一個標準的 Spring MVC Controller,在此不詳細展開討論。僅僅需要說明的是這個類型注入了 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 改造目標

上一章,我們已經(jīng)成功構(gòu)建了一個模擬系統(tǒng),該系統(tǒng)是一個單體系統(tǒng),對外提供了兩個 RESTful 接口。本章要達到的目標是將該單體系統(tǒng)拆分為兩個獨立運行的微服務(wù)系統(tǒng)。如 2.2 節(jié)所述,進行模塊化拆分是實施微服務(wù)改造的重要一步,因為在接下來的描述中會暗含一個約定:hello-web、hello-core hello-api 這三個模塊與上一章中所設(shè)定的能力是相同的。基于 1.7 節(jié)所提到的“盡量少改動(最好不改動)遺留系統(tǒng)的內(nèi)部代碼”的改造要求,這三個模塊中的代碼是不會被大面積修改的,只會有些許調(diào)整,以適應(yīng)新的微服務(wù)環(huán)境。

具體將要實現(xiàn)的目標效果如下:


第一個微服務(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 模塊來遠程調(diào)用 hello-service-provider 系統(tǒng)才能完成業(yè)務(wù)。而 hello-service-provider 系統(tǒng)則需要暴露可供 hello-service-reference 模塊調(diào)用的遠程接口,并實現(xiàn) Service 及 DAO 接口定義的具體業(yè)務(wù)邏輯。

本章節(jié)就是要重點介紹 hello-service-provider 和 hello-service-reference 模塊是如何構(gòu)建的,以及它們在微服務(wù)改造過程中所起到的作用。

3.2 暴露遠程服務(wù)

Spring Boot 和 Dubbo 的結(jié)合使用可以引入諸如 spring-boot-starter-dubbo 這樣的起始包,使用起來會更加方便。但是考慮到項目的單純性和通用性,本文仍然延用 Spring 經(jīng)典的方式進行配置。

首先,我們需要創(chuàng)建一個新的模塊,叫做 hello-service-provider,這個模塊的作用是用來暴露遠程服務(wù)接口的。依托于 Dubbo 強大的服務(wù)暴露及整合能力,該模塊不用編寫任何代碼,僅需添加一些配置即可完成。

注:有關(guān) Dubbo 的具體使用和配置說明并不是本文討論的重點,請參考官方文檔。

3.2.1 添加 dubbo-services.xml 文件

dubbo-services.xml 配置是該模塊的關(guān)鍵,Dubbo 就是根據(jù)這個文件,自動暴露遠程服務(wù)的。這是一個標準 Spring 風(fēng)格的配置文件,引入了 Dubbo 命名空間,需要將其擺放在 src/main/resources/META-INF/spring 目錄下,這樣 Maven 在打包的時候會自動將其添加到 classpath。

3.2.2 添加 POM 文件

有關(guān) Maven 的使用與配置也不是本文關(guān)注的重點,但是這個模塊用到了一些 Maven 插件,在此對這些插件的功能和作用進行一下描述。





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 文件中對其進行配置。該文件需要擺放在 src/main/resources 目錄下,有關(guān)該配置文件的具體內(nèi)容請參見代碼倉庫,有關(guān)配置的詳細解釋,請參考官方文檔。

####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 機制進行服務(wù)注冊與發(fā)現(xiàn),需要添加-Djava.net.preferIPv4Stack=true 參數(shù),否則會拋出異常。

可以使用如下命令來判斷服務(wù)是否正常運行:


$ netstat -antl | grep 20880

如果有類似如下的信息輸出,則說明運行正常。

如果是在正式環(huán)境運行,就需要將上一步生成的壓縮包解壓,然后運行 bin 目錄下的相應(yīng)腳本即可。

3.2.7 總結(jié)

使用這種方式來暴露遠程服務(wù)具有如下一些優(yōu)勢:

使用 Dubbo 進行遠程服務(wù)暴露,無需關(guān)注底層實現(xiàn)細節(jié)

對原系統(tǒng)沒有任何入侵,已有系統(tǒng)可以繼續(xù)按照原來的方式啟動和運行

暴露過程可插拔

Dubbo 服務(wù)與原有服務(wù)在開發(fā)期和運行期均可以共存

無需編寫任何代碼

3.3 引用遠程服務(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-corehello-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)的,其目的就是遠程調(diào)用 hello-service-provider 暴露出來的服務(wù),并提供本地代理。

這時 hello-web 模塊的依賴關(guān)系就發(fā)生了變化:原來 hello-web 模塊直接依賴 hello-core,再通過 hello-core 間接依賴 hello-api,而現(xiàn)在我們需要將其改變?yōu)橹苯右蕾?hello-service-reference 模塊,再通過 hello-service-reference 模塊間接依賴 hello-api。改造前后的依賴關(guān)系分別為:

3.3.2 啟動服務(wù)

因為是測試環(huán)境,只需要執(zhí)行以下命令即可,但在進行本操作之前,需要先啟動 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;

...
}

顯然,helloServiceuserService 都是無法注入的,這是為什么呢?

原因自然跟我們修改 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 的配置文件中聲明了兩個對遠程服務(wù)的引用,按道理來說這個注入應(yīng)該是可以生效的,但顯然實際情況并非如此。

仔細思考不難發(fā)現(xiàn),我們在執(zhí)行 mvn exec:java 命令啟動 hello-service-provider 模塊的時候指定了啟動 com.alibaba.dubbo.container.Main 類型,然后才會開始啟動并加載 Dubbo 的有關(guān)配置,這一點從日志中可以得到證實(日志里面會打印出來很多帶有 [DUBBO] 標簽的內(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 '

等等,什么!括號里面的創(chuàng)建時間為什么是 N/A,這說明 createdTime 字段根本沒有值!

3.4 保持副作用效果

讓我們先來回顧一下 1.6 節(jié)所提到的副作用效果。在 DefaultUserService.create 方法中,我們?yōu)閭魅氲?user 參數(shù)設(shè)置了創(chuàng)建時間,這一操作就是我們要關(guān)注的具有副作用效果的操作。

先說單體系統(tǒng)的情況。單體系統(tǒng)是運行在一個 Java 虛擬機中的,所有對象共享一片內(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)是獨立運行在兩個虛擬機中的,彼此之間的內(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ù)以后,先進行反序列化,生成跟原來對象一模一樣的副本,再由 UserService.create 方法進行處理(這回調(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 字段確實是有值的。

那么該如何讓這個副作用效果也能夠被處于另外一個虛擬機中的 hello-web 系統(tǒng)感知到呢,方法只有一種,就是將變更后的數(shù)據(jù)回傳。

3.4.1 為方法添加返回值

這是最容易想到的一種實現(xiàn)方式,簡單的說就是修改服務(wù)接口,將變更后的數(shù)據(jù)返回。

首先,修改 UserService 接口的 create 方法,添加返回值:

// 為方法添加返回值
User create(User user);
}

然后,修改實現(xiàn)類中相應(yīng)的方法,將變更后的 user 對象返回:

@Override
public User create(User user) {

}
}

最后,修改調(diào)用方實現(xiàn),接收返回值:

@RequestMapping(path = '/create', method = RequestMethod.POST)
public String createUser(@RequestParam('userId') String userId, @RequestParam('name') String name) {

}
}

編譯、運行并測試(如下圖),正如我們所期望的,括號中的創(chuàng)建時間又回來了。其工作原理與本節(jié)開始時所描述的是一樣的,只是方向相反而已。在此不再詳細展開,留給大家自行思考。

這種修改方式有如下一些優(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ī)范不建議使用這樣的標識符來為方法命名;第二、在原有方法前加上 __rpc_ 前綴,能夠做到與原有方法對應(yīng),便于閱讀和理解。示例如下:

// 保持原有方法不變
void create(User user);

// 添加一個方法,新方法需要有返回值
User __rpc_create(User user);
}

然后,在實現(xiàn)類中實現(xiàn)這個新方法:

// 保持原有方法實現(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 UserServiceStub(UserService userService) {

}

@Override
public User getById(String id) {

}

@Override
public void create(User user) {

}

@Override
public User __rpc_create(User user) {

}
}

該類型即為本地存根。簡單來說,就是在調(diào)用方調(diào)用本地代理的方法之前,會先去調(diào)用本地存根中相應(yīng)的方法,因此本地存根與服務(wù)提供方和服務(wù)引用方需要實現(xiàn)同樣的接口。本地存根中的構(gòu)造函數(shù)是必須的,且方法簽名也是被約定好的——需要傳入本地代理作為參數(shù)。其中 getById __rpc_create 方法都是直接調(diào)用了本地代理中的方法,不必過多關(guān)注,重點來說說 create 方法。首先,create 調(diào)用了本地存根中的 __rpc_create 方法,這個方法透過本地代理訪問到了服務(wù)提供方的相應(yīng)方法,并成功接收了返回值 newUser,這個返回值是包含修改后的 createdTime 字段的,于是我們要做的事情就是從 newUser 對象里面獲取到 createdTime 字段的值,并設(shè)置給 user 參數(shù),以達到產(chǎn)生副作用的效果。此時 user 參數(shù)會帶著新設(shè)置的 createdTime 的值,將其“傳遞”給 create 方法的調(diào)用方。

最后,在 dubbo-references.xml 文件中修改一處配置,以啟用該本地存根:

鑒于本地存根的工作機制,我們是不需要修改調(diào)用方 hello-web 模塊中的任何代碼及配置的。編譯、運行并測試,同樣可以達到我們想要的效果。

這種實現(xiàn)方式會比第一種方式改進不少,但也有致命弱點:

保持了接口的向后兼容性

引入本地存根,無需修改調(diào)用方代碼

通過配置可以實現(xiàn)改造結(jié)果的可插拔

實現(xiàn)復(fù)雜,尤其是本地存根的實現(xiàn),如果遺留系統(tǒng)的代碼對傳入?yún)?shù)里的內(nèi)容進行了無節(jié)制的修改的話,那么重現(xiàn)該副作用效果是非常耗時且容易出錯的

難以理解

四、總結(jié)

至此,將遺留系統(tǒng)改造為微服務(wù)系統(tǒng)的任務(wù)就大功告成了,而且基本上滿足了文章最開始提出來的十點改造原則與要求(此處應(yīng)給自己一些掌聲),不知道是否對大家有所幫助?雖然示例項目是為了敘述要求而量身定制的,但文章中提到的種種理念與方法卻實實在在是從實踐中摸索和總結(jié)出來的——踩過的坑,遇到的問題,解決的思路以及改造的難點等都一一呈現(xiàn)給了大家。

微服務(wù)在當(dāng)下已經(jīng)不是什么新鮮的技術(shù)了,但歷史包袱依然是限制其發(fā)展的重要因素,希望這篇文章能帶給大家一點啟發(fā),在接下來的工作中更好的擁抱微服務(wù)帶來的變革。

打開APP,閱讀全文并永久保存 查看更多類似文章
猜你喜歡
類似文章
零基礎(chǔ)都秒懂:手把手教你搭建一套微服務(wù)框架!
dubbo項目實戰(zhàn)代碼展示
白話分布式系統(tǒng)
在Liferay Portal Professional里實現(xiàn)一個使用SOAP的portlet
Axis開發(fā)WebService(stubs方式)
在Ubuntu為Android硬件抽象層(HAL)模塊編寫JNI方法提供Java訪問硬件服...
更多類似文章 >>
生活服務(wù)
分享 收藏 導(dǎo)長圖 關(guān)注 下載文章
綁定賬號成功
后續(xù)可登錄賬號暢享VIP特權(quán)!
如果VIP功能使用有故障,
可點擊這里聯(lián)系客服!

聯(lián)系客服