天天看點

使用 Dubbo 對遺留單體系統進行微服務改造編譯、打包和安裝在項目根目錄下執行指令運作在 hello-web 目錄下執行指令

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 月份的

《技術雷達》 中,ThoughtWorks 給予了微服務很高的評價。同時,也有越來越多的組織将實施微服務作為架構演進的一個必選方向。隻不過在擁有衆多遺留系統的組織内,将曾經的單體系統拆分為微服務并不是一件容易的事情。本文将從對遺留系統進行微服務改造的原則要求出發,探讨如何使用 Dubbo 架構實作單體系統向微服務的遷移。

一、原則要求

想要對标準三層架構的單體系統進行微服務改造——簡言之——就是将曾經單一程序内服務之間的本地調用改造為跨程序的分布式調用。這雖然不是微服務改造的全部内容,但卻直接決定了改造前後的系統能否保持相同的業務能力,以及改造成本的多少。

1.1 适合的架構

在微服務領域,雖然技術棧衆多,但無非 RPC 與 RESTful 兩個流派,這其中最具影響力的代表當屬 Dubbo 與 Spring Cloud 了 。他們擁有相似的能力,卻有着截然不同的實作方式——本文并不是想要對微服務架構的選型過程進行深入剖析,也不想對這兩種架構的孰優孰劣進行全面比較——本章所提到的全部這些原則要求都是超越具體實作的,其之于任何微服務架構都應該是适用的。讀者朋友們大可以把本文中的 Dubbo 全部替換為 Spring Cloud,而并不會對最終結果造成任何影響,唯一需要改變的僅僅是實作的細節過程而已。是以,無論最後抉擇如何,都是無所謂對錯的,關鍵在于:要選擇符合組織當下現狀的最适合的那一個。

1.2 友善的将服務暴露為遠端接口

單體系統,服務之間的調用是在同一個程序内完成的;而微服務,是将獨立的業務子產品拆分到不同的應用系統中,每個應用系統可以作為獨立的程序來部署和運作。是以進行微服務改造,就需要将程序内方法調用改造為程序間通信。程序間通信的實作方式有很多種,但顯然基于網絡調用的方式是最通用且易于實作的。那麼能否友善的将本地服務暴露為網絡服務,就決定了暴露過程能否被快速實施,同時暴露的過程越簡單則暴露後的接口與之前存在不一緻性的風險也就越低。

1.3 友善的生成遠端服務調用代理

當服務被暴露為遠端接口以後,程序内的本地實作将不複存在。簡化調用方的使用——為遠端服務生成相應的本地代理,将底層網絡互動細節進行深層次的封裝——就顯得十分必要。另外遠端服務代理在使用與功能上不應該與原有本地實作有任何差别。

1.4 保持原有接口不變或向後相容

在微服務改造過程中,要確定接口不變或向後相容,這樣才不至于對調用方産生巨大影響。在實際操作過程中,我們有可能僅僅可以掌控被改造的系統,而無法通路或修改調用方系統。倘若接口發生重大變化,調用方系統的維護人員會難以接受:這會對他們的工作産生不可預估的風險和沖擊,還會因為适配新接口而産生額外的工作量。

1.5 保持原有的依賴注入關系不變

基于 Spring 開發的遺留系統,服務之間通常是以依賴注入的方式彼此關聯的。進行微服務改造後,原本注入的服務實作變成了本地代理,為了盡量減少代碼變更,最好能夠自動将注入的實作類切換為本地代理。

1.6 保持原有代碼的作用或副作用效果不變

這一點看上去有些複雜,但卻是必不可少的。改造後的系統跟原有系統保持相同的業務能力,當且僅當改造後的代碼與原有代碼保持相同的作用甚至是副作用。這裡要額外提及的是副作用。我們在改造過程中可以很好的關注一般作用效果,卻往往會忽視副作用的影響。舉個例子,Java 内部進行方法調用的時候參數是以引用的方式傳遞的,這意味着在方法體中可以修改參數裡的值,并将修改後的結果“傳回”給被調用方。看下面的例子會更容易了解:

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}

}

這段代碼在同一個程序中運作是沒有問題的,因為兩個方法共享同一片記憶體空間,

innerMethod

 對 

map

 的修改可以直接反映到 

outerMethod

 方法中。但是在微服務場景下事實就并非如此了,此時 

innerMethod

 和 

outerMethod

 運作在兩個獨立的程序中,程序間的記憶體互相隔離,

innerMethod

修改的内容必須要主動回傳才能被 

outerMethod

 接收到,僅僅修改參數裡的值是無法達到回傳資料的目的的。

此處副作用的概念是指在方法體中對傳入參數的内容進行了修改,并由此對外部上下文産生了可察覺的影響。顯然副作用是不友好且應該被避免的,但由于是遺留系統,我們不能保證其中不會存在諸如此類寫法的代碼,是以我們還是需要在微服務改造過程中,對副作用的影響效果進行保持,以獲得更好的相容性。

1.7 盡量少改動(最好不改動)遺留系統的内部代碼

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

1.8 良好的容錯能力

不同于程序内調用,跨程序的網絡通信可靠性不高,可能由于各種原因而失敗。是以在進行微服務改造的時候,遠端方法調用需要更多考慮容錯能力。當遠端方法調用失敗的時候,可以進行重試、恢複或者降級,否則不加處理的失敗會沿着調用鍊向上傳播(冒泡),進而導緻整個系統的級聯失敗。

1.9 改造結果可插拔

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

1.10 更多

當然微服務改造的要求遠不止上面提到的這些點,還應該包括諸如:配置管理、服務注冊與發現、負載均衡、網關、限流降級、擴縮容、監控和分布式事務等,然而這些需求大部分是要在微服務系統已經更新改造完畢,複雜度不斷增加,流量上升到一定程度之後才會遇到和需要的,是以并不是本文關注的重點。但這并不意味着這些内容就不重要,沒有他們微服務系統同樣也是無法正常、平穩、高速運作的。

二、模拟一個單體系統

2.1 系統概述

我們需要建構一個具有三層架構的單體系統來模拟遺留系統,這是一個簡單的 Spring Boot 應用,項目名叫做 

hello-dubbo

。本文涉及到的所有源代碼均可以到 

Github

 上檢視和下載下傳。

首先,系統存在一個模型

 User

 和對該模型進行管理的 DAO,并通過 

UserService

 向上層暴露通路 User 模型的接口;另外,還存在一個 

HelloService

,其調用 

UserService

 并傳回一條問候資訊;之後,由 

Controller

 對外暴露 RESTful 接口;最終再通過 Spring Boot 的 

Application

 整合成一個完整應用。

2.2 子產品化拆分

通常來說,一個具有三層架構的單體系統,其 Controller、Service 和 DAO 是存在于一整個子產品内的,如果要進行微服務改造,就要先對這個整體進行拆分。拆分的方法是以 Service 層為分界,将其分割為兩個子子產品:Service 層往上作為一個子子產品(稱為

 hello-web

),對外提供 RESTful 接口;Service 層往下作為另外一個子子產品(稱為 

hello-core

),包括 Service、DAO 以及模型。

hello-core

 被 

hello-web

 依賴。當然,為了更好的展現面向契約的程式設計精神,可以把

 hello-core 

再進一步拆分:所有的接口和模型都獨立出來,形成 

hello-api

,而 

hello-core

 依賴

 hello-api

。最終,拆分後的子產品關系如下:

hello-dubbo
|-- hello-web(包含 Application 和 Controller)      
|-- hello-core(包含 Service 和 DAO 的實作)
    |-- 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,實作了 

Serializable

 接口(因為模型資料要在網絡上傳輸,是以必須能夠支援序列化和反序列化)。為了友善控制台輸出,這裡覆寫了預設的

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

public User getById(String id) {

return STORE.get(id);           

public void create(User user) {

STORE.put(user.getId(), user);           

InMemoryUserRepository

 是 

UserRepository

 接口的實作類。該類型使用一個 

Map

 對象 

STORE

來存儲資料,并通過靜态代碼塊向該對象内添加了一個預設使用者。

getById

 方法根據

 id 

參數從 

STORE

 中擷取使用者資料,而 

create

 方法就是簡單将傳入的 

user

 對象存儲到 

STORE

 中。由于所有這些操作都隻是在記憶體中完成的,是以該類型被叫做

 InMemoryUserRepository

2.3.4 UserService

public interface UserService {
  User getById(String id);      

與 

UserRepository

 的方法一一對應,向更上層暴露通路接口。

2.3.5 DefaultUserService

@Service("userService")
public class DefaultUserService implements UserService {
  private static final Logger LOGGER = LoggerFactory.getLogger(DefaultUserService.class);      

@Autowired

private UserRepository userRepository;

User user = this.userRepository.getById(id);
LOGGER.info(user.toString());
return user;           
user.setCreatedTime(new Date());
this.userRepository.create(user);
LOGGER.info(user.toString());           

DefaultUserService

UserService

 接口的預設實作,并通過 

@Service

 注解聲明為一個服務,服務 id 為 

userService

(該 id 在後面會需要用到)。該服務内部注入了一個 

UserRepository

類型的對象 

userRepository

getUserById

 方法根據 id 從 

userRepository

 中擷取資料,而

 createUser 

方法則将傳入的

 user 

參數通過

 userRepository.create

 方法存入,并在存入之前設定了該對象的建立時間。很顯然,根據 1.6 節關于副作用的描述,為

 user

 對象設定建立時間的操作就屬于具有副作用的操作,需要在微服務改造之後加以保留。為了友善看到系統工作效果,這兩個方法裡面都列印了日志。

2.3.6 HelloService

public interface HelloService {
  String sayHello(String userId);
}
      

HelloService

 接口隻提供一個方法

sayHello

,就是根據傳入的

userId

 傳回一條對該使用者的問候資訊。

2.3.7 DefaultHelloService

@Service("helloService")
public class DefaultHelloService implements HelloService {
  @Autowired
  private UserService userService;      

public String sayHello(String userId) {

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

DefaultHelloService

HelloService

 接口的預設實作,并通過 @Service 注解聲明為一個服務,服務 id 為 

helloService

(同樣,該名稱在後面的改造過程中會被用到)。該類型内部注入了一個 

UserService

 類型的對象 

userService

sayHello

 方法根據 

userId

 參數通過

 userService

 擷取使用者資訊,并傳回一條經過格式化後的消息。

2.3.8 Application

@SpringBootApplication
public class Application {
  public static void main(String[] args) throws Exception {      
SpringApplication.run(Application.class, args);           

Application

 類型是 Spring Boot 應用的入口,較長的描述請參考 Spring Boot 的

官方文檔

,在此不詳細展開。

2.3.9 Controller

@RestController
public class Controller {
  @Autowired
  private HelloService helloService;      

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 

方法中調用了這兩個對象中的有關方法。

2.4 打包運作

hello-dubbo

 項目包含三個子子產品:

hello-api

hello-core 

和 

hello-web

,是用 Maven 來管理的。到目前為止所涉及到的 POM 檔案都比較簡單,為了節約篇幅,就不在此一一列出了,感興趣的朋友可以到項目的 Github 倉庫上自行研究。

hello-dubbo

 項目的打包和運作都非常直接:

編譯、打包和安裝

在項目根目錄下執行指令

$ mvn clean install

運作

在 hello-web 目錄下執行指令

$ mvn spring-boot:run

使用 Dubbo 對遺留單體系統進行微服務改造編譯、打包和安裝在項目根目錄下執行指令運作在 hello-web 目錄下執行指令

測試結果如下,注意每次輸出括号裡面的日期時間,它們都應該是有值的。

使用 Dubbo 對遺留單體系統進行微服務改造編譯、打包和安裝在項目根目錄下執行指令運作在 hello-web 目錄下執行指令

再傳回 

hello-web

 系統的控制台,檢視一下日志輸出,時間應該與上面是一樣的。

使用 Dubbo 對遺留單體系統進行微服務改造編譯、打包和安裝在項目根目錄下執行指令運作在 hello-web 目錄下執行指令

三、動手改造

3.1 改造目标

上一章,我們已經成功建構了一個模拟系統,該系統是一個單體系統,對外提供了兩個 RESTful 接口。本章要達到的目标是将該單體系統拆分為兩個獨立運作的微服務系統。如 2.2 節所述,進行子產品化拆分是實施微服務改造的重要一步,因為在接下來的描述中會暗含一個約定:

hello-web

hello-core

 hello-api

 這三個子產品與上一章中所設定的能力是相同的。基于 1.7 節所提到的“盡量少改動(最好不改動)遺留系統的内部代碼”的改造要求,這三個子產品中的代碼是不會被大面積修改的,隻會有些許調整,以适應新的微服務環境。

具體将要實作的目标效果如下:

第一個微服務系統:      

hello-web(包含 Application 和 Controller)

|-- hello-service-reference(包含 Dubbo 有關服務引用的配置)

|-- hello-api(包含 Service 和 DAO 的接口以及模型)
           

第二個微服務系統:

hello-service-provider(包含 Dubbo 有關服務暴露的配置)

|-- hello-core(包含 Service 和 DAO 的實作)

|-- hello-api(包含 Service 和 DAO 的接口以及模型)           

hello-web

 與原來一樣,是一個面向最終使用者提供 Web 服務的終端系統,其隻包含 Application、Controller、Service 接口、 DAO 接口以及模型,是以它本身是不具備任何業務能力的,必須通過依賴

 hello-service-referenc

e 子產品來遠端調用 

hello-service-provider

 系統才能完成業務。而 

hello-service-provide

r 系統則需要暴露可供

 hello-service-reference

 子產品調用的遠端接口,并實作 Service 及 DAO 接口定義的具體業務邏輯。

本章節就是要重點介紹

 hello-service-provide

r 和 

hello-service-reference

 子產品是如何建構的,以及它們在微服務改造過程中所起到的作用。

3.2 暴露遠端服務

Spring Boot 和 Dubbo 的結合使用可以引入諸如 

spring-boot-starter-dubbo

 這樣的起始包,使用起來會更加友善。但是考慮到項目的單純性和通用性,本文仍然延用 Spring 經典的方式進行配置。

首先,我們需要建立一個新的子產品,叫做

 hello-service-provider

,這個子產品的作用是用來暴露遠端服務接口的。依托于 Dubbo 強大的服務暴露及整合能力,該子產品不用編寫任何代碼,僅需添加一些配置即可完成。

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

3.2.1 添加 dubbo-services.xml 檔案

dubbo-services.xml

 配置是該子產品的關鍵,Dubbo 就是根據這個檔案,自動暴露遠端服務的。這是一個标準 Spring 風格的配置檔案,引入了 Dubbo 命名空間,需要将其擺放在 

src/main/resources/META-INF/spring

 目錄下,這樣 Maven 在打包的時候會自動将其添加到 classpath。

使用 Dubbo 對遺留單體系統進行微服務改造編譯、打包和安裝在項目根目錄下執行指令運作在 hello-web 目錄下執行指令

3.2.2 添加 POM 檔案

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

使用 Dubbo 對遺留單體系統進行微服務改造編譯、打包和安裝在項目根目錄下執行指令運作在 hello-web 目錄下執行指令
使用 Dubbo 對遺留單體系統進行微服務改造編譯、打包和安裝在項目根目錄下執行指令運作在 hello-web 目錄下執行指令
使用 Dubbo 對遺留單體系統進行微服務改造編譯、打包和安裝在項目根目錄下執行指令運作在 hello-web 目錄下執行指令
使用 Dubbo 對遺留單體系統進行微服務改造編譯、打包和安裝在項目根目錄下執行指令運作在 hello-web 目錄下執行指令
使用 Dubbo 對遺留單體系統進行微服務改造編譯、打包和安裝在項目根目錄下執行指令運作在 hello-web 目錄下執行指令

3.2.3添加 assembly.xml 檔案

Assembly 插件的主要功能是對項目重新打包,以便自定義打包方式和内容。對本項目而言,需要生成一個壓縮包,裡面包含所有運作該服務所需要的 jar 包、配置檔案和啟動腳本等。Assembly 插件需要

assembly.xml

檔案來描述具體的打包過程,該檔案需要擺放在

src/main/assembly

目錄下。有關

assembly.xml

檔案的具體配置方法,請參考

使用 Dubbo 對遺留單體系統進行微服務改造編譯、打包和安裝在項目根目錄下執行指令運作在 hello-web 目錄下執行指令
使用 Dubbo 對遺留單體系統進行微服務改造編譯、打包和安裝在項目根目錄下執行指令運作在 hello-web 目錄下執行指令

3.2.4 添加 logback.xml 檔案

由于在 POM 檔案中指定了使用 logback 作為日志輸出元件,是以還需要在

logback.xml

檔案中對其進行配置。該檔案需要擺放在

src/main/resources

目錄下,有關該配置檔案的具體内容請參見代碼倉庫,有關配置的詳細解釋,請參考

使用 Dubbo 對遺留單體系統進行微服務改造編譯、打包和安裝在項目根目錄下執行指令運作在 hello-web 目錄下執行指令

####3.2.5 打包

由于已經在 POM 檔案中定義了打包的相關配置,是以直接在

hello-service-provider

目錄下運作以下指令即可:

$ mvn clean package
      

成功執行以後,會在其 target 目錄下生成一個名為

hello-service-provider-0.1.0-SNAPSHOT-assembly.tar.gz

的壓縮包,裡面的内容如圖所示:

使用 Dubbo 對遺留單體系統進行微服務改造編譯、打包和安裝在項目根目錄下執行指令運作在 hello-web 目錄下執行指令

3.2.6 運作

如此配置完成以後,就可以使用如下指令來啟動服務:

$ MAVEN_OPTS="-Djava.net.preferIPv4Stack=true" mvn exec:java
      
注:在 macOS 系統裡,使用 multicast 機制進行服務注冊與發現,需要添加

-Djava.net.preferIPv4Stack=true

參數,否則會抛出異常。

可以使用如下指令來判斷服務是否正常運作:

$ netstat -antl | grep 20880
      

如果有類似如下的資訊輸出,則說明運作正常。

使用 Dubbo 對遺留單體系統進行微服務改造編譯、打包和安裝在項目根目錄下執行指令運作在 hello-web 目錄下執行指令

如果是在正式環境運作,就需要将上一步生成的壓縮包解壓,然後運作

bin

目錄下的相應腳本即可。

3.2.7 總結

使用這種方式來暴露遠端服務具有如下一些優勢:

使用 Dubbo 進行遠端服務暴露,無需關注底層實作細節

對原系統沒有任何入侵,已有系統可以繼續按照原來的方式啟動和運作

暴露過程可插拔

Dubbo 服務與原有服務在開發期和運作期均可以共存

無需編寫任何代碼

3.3 引用遠端服務

3.3.1 添加服務引用

hello-service-provider

子產品的處理方式相同,為了不侵入原有系統,我們建立另外一個子產品,叫做

hello-service-reference

。這個子產品隻有一個配置檔案

dubbo-references.xml

放置在

src/main/resources/META-INF/spring/

目錄下。檔案的内容非常簡單明了:

使用 Dubbo 對遺留單體系統進行微服務改造編譯、打包和安裝在項目根目錄下執行指令運作在 hello-web 目錄下執行指令

但不同于

hello-service-provider

子產品的一點在于,該子產品隻需要打包成一個 jar 即可,POM 檔案内容如下:

使用 Dubbo 對遺留單體系統進行微服務改造編譯、打包和安裝在項目根目錄下執行指令運作在 hello-web 目錄下執行指令

總結一下,我們曾經的遺留系統分為三個子產品

hello-web

,

hello-core

hello-api

。經過微服務化處理以後,

hello-core

hello-api

被剝離了出去,加上

hello-service-provider

子產品,形成了一個可以獨立運作的

hello-service-provider

系統,是以需要打包成一個完整的應用;而

hello-web

要想調用

hello-core

提供的服務,就不能再直接依賴

hello-core

子產品了,而是需要依賴我們這裡建立的

hello-service-reference

子產品,是以

hello-service-reference

是作為一個依賴庫出現的,其目的就是遠端調用

hello-service-provider

暴露出來的服務,并提供本地代理。

這時

hello-web

子產品的依賴關系就發生了變化:原來

hello-web

子產品直接依賴

hello-core

,再通過

hello-core

間接依賴

hello-api

,而現在我們需要将其改變為直接依賴

hello-service-reference

子產品,再通過

hello-service-reference

子產品間接依賴

hello-api

。改造前後的依賴關系分别為:

使用 Dubbo 對遺留單體系統進行微服務改造編譯、打包和安裝在項目根目錄下執行指令運作在 hello-web 目錄下執行指令

3.3.2 啟動服務

因為是測試環境,隻需要執行以下指令即可,但在進行本操作之前,需要先啟動

hello-service-provider

服務。

$ MAVEN_OPTS="-Djava.net.preferIPv4Stack=true" mvn spring-boot:run
      

Oops!系統并不能像期望的那樣正常運作,會抛出如下異常:

使用 Dubbo 對遺留單體系統進行微服務改造編譯、打包和安裝在項目根目錄下執行指令運作在 hello-web 目錄下執行指令

意思是說

net.tangrui.demo.dubbo.hello.web.Controller

這個類的

helloService

字段需要一個類型為

net.tangrui.demo.dubbo.hello.service.HelloService

的 Bean,但是沒有找到。相關代碼片段如下:

@RestController
Public class Controller {
  @Autowired
  private HelloService helloService;      

...

顯然,

helloService

userService

都是無法注入的,這是為什麼呢?

原因自然跟我們修改

hello-web

這個子產品的依賴關系有關。原本

hello-web

是依賴于

hello-core

的,

hello-core

裡面聲明了

HelloService

UserService

這兩個服務(通過

@Service

注解),然後

Controller

@Autowired

的時候就可以自動綁定了。但是,現在我們将

hello-core

替換成了

hello-service-reference

,在

hello-service-reference

的配置檔案中聲明了兩個對遠端服務的引用,按道理來說這個注入應該是可以生效的,但顯然實際情況并非如此。

仔細思考不難發現,我們在執行

mvn exec:java

指令啟動

hello-service-provider

子產品的時候指定了啟動

com.alibaba.dubbo.container.Main

類型,然後才會開始啟動并加載 Dubbo 的有關配置,這一點從日志中可以得到證明(日志裡面會列印出來很多帶有

[DUBBO]

标簽的内容),顯然在這次運作中,我們并沒有看到類似這樣的日志,說明 Dubbo 在這裡沒有被正确啟動。歸根結底還是 Spring Boot 的原因,即 Spring Boot 需要一些配置才能夠正确加載和啟動 Dubbo。

讓 Spring Boot 支援 Dubbo 有很多種方法,比如前面提到的

spring-boot-starter-dubbo

起始包,但這裡同樣為了簡單和通用,我們依舊采用經典的方式來解決。

繼續思考,該子產品沒有成功啟動 Dubbo,僅僅是因為添加了對

hello-service-reference

的引用,而

hello-service-reference

子產品就隻有一個檔案

dubbo-references.xml

,這就說明 Spring Boot 并沒有加載到這個檔案。順着這個思路,隻需要讓 Spring Boot 能夠成功加載這個檔案,問題就可以了。Spring Boot 也确實提供了這樣的能力,隻可惜無法完全做到代碼無侵入,隻能說這些改動是可以被接受的。修改方式是替換

Application

中的注解(至于為什麼要修改成這樣的結果,超出了本文的讨論範圍,請自行 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

就是我們需要的。

這時再重新嘗試啟動,就一切正常了。

使用 Dubbo 對遺留單體系統進行微服務改造編譯、打包和安裝在項目根目錄下執行指令運作在 hello-web 目錄下執行指令

但是,我們如何驗證結果确實是從

hello-service-provider

服務過來的呢?這時就需要用到

DefaultUserService

裡面的那幾行日志輸出了,回到

hello-service-provider

服務的控制台,能夠看到類似這樣的輸出:

使用 Dubbo 對遺留單體系統進行微服務改造編譯、打包和安裝在項目根目錄下執行指令運作在 hello-web 目錄下執行指令

如此便可以确信系統的拆分是被成功實作了。再試試建立使用者的接口:

$ curl -X POST '        http://127.0.0.1:8080/create?userId=huckleberry&name=Huckleberry%20Finn               '
      
使用 Dubbo 對遺留單體系統進行微服務改造編譯、打包和安裝在項目根目錄下執行指令運作在 hello-web 目錄下執行指令

等等,什麼!括号裡面的建立時間為什麼是

N/A

,這說明

createdTime

字段根本沒有值!

3.4 保持副作用效果

讓我們先來回顧一下 1.6 節所提到的副作用效果。在

DefaultUserService.create

方法中,我們為傳入的

user

參數設定了建立時間,這一操作就是我們要關注的具有副作用效果的操作。

先說單體系統的情況。單體系統是運作在一個 Java 虛拟機中的,所有對象共享一片記憶體空間,彼此可以互相通路。系統在運作的時候,先是由

Controller.create

方法擷取使用者輸入,将輸入的參數封裝為一個

user

對象,再傳遞給

UserService.create

方法(具體是在調用

DefaultUserService.create

方法),這時

user

對象的

createdTime

字段就被設定了。由于 Java 是以引用的方式來傳遞參數,是以在

create

方法中對

user

對象所做的變更,是能夠反映到調用方那裡的——即

Controller.create

方法裡面也是可以擷取到變更的,是以傳回給使用者的時候,這個

createdTime

就是存在的。

再說微服務系統的情況。此時系統是獨立運作在兩個虛拟機中的,彼此之間的記憶體是互相隔離的。起始點同樣是

hello-web

系統的 Controller.create 方法:擷取使用者輸入,封裝 user 對象。可是在調用 UserService.create 方法的時候,并不是直接調用

DefaultUserService

中的方法,而是調用了一個具有相同接口的本地代理,這個代理将

user

對象序列化之後,通過網絡傳輸給了

hello-service-provider

系統。該系統接收到資料以後,先進行反序列化,生成跟原來對象一模一樣的副本,再由

UserService.create

方法進行處理(這回調用的就是

DefaultUserService

裡面的實作了)。至此,這個被設定過

createdTime

user

對象副本是一直存在于

hello-service-provider

系統的記憶體裡面的,從來沒有被傳遞出去,自然是無法被

hello-web

系統讀取到的,是以最終列印出來的結果,括号裡面的内容就是

N/A

了。記得我們有在

DefaultUserService.create

方法中輸出過日志,是以回到

hello-service-provider

系統的控制台,可以看到如下的日志資訊,說明在這個系統裡面

createdTime

字段确實是有值的。

使用 Dubbo 對遺留單體系統進行微服務改造編譯、打包和安裝在項目根目錄下執行指令運作在 hello-web 目錄下執行指令

那麼該如何讓這個副作用效果也能夠被處于另外一個虛拟機中的

hello-web

系統感覺到呢,方法隻有一種,就是将變更後的資料回傳。

3.4.1 為方法添加傳回值

這是最容易想到的一種實作方式,簡單的說就是修改服務接口,将變更後的資料傳回。

首先,修改

UserService

接口的

create

方法,添加傳回值:

public interface UserService {
  ...      

// 為方法添加傳回值

User create(User user);

然後,修改實作類中相應的方法,将變更後的

user

對象傳回:

@Service("userService")
public class DefaultUserService implements UserService {
  ...      

public User create(User user) {

user.setCreatedTime(new Date());
this.userRepository.create(user);
LOGGER.info(user.toString());
// 将變更後的資料傳回
return user;           

最後,修改調用方實作,接收傳回值:

@RestController
public class Controller {
  ...      
User user = new User();
user.setId(userId);
user.setName(name);

// 接收傳回值
User newUser = this.userService.create(user);
return newUser.toString();           

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

使用 Dubbo 對遺留單體系統進行微服務改造編譯、打包和安裝在項目根目錄下執行指令運作在 hello-web 目錄下執行指令

這種修改方式有如下一些優缺點:

方法簡單,容易了解

改變了系統接口,且改變後的接口與原有接口不相容(違背了 1.4 節關于“保持原有接口不變或向後相容”原則的要求)

由此也不可避免的造成了對遺留系統内部代碼的修改(違背了 1.7 節關于“盡量少改動(最好不改動)遺留系統的内部代碼”原則的要求)

修改方式不可插拔(違背了 1.9 節“改造結果可插拔”原則的要求)

由此可見,這種改法雖然簡單,卻是利大于弊的,除非我們能夠完全掌控整個系統,否則這種修改方式的風險會随着系統複雜性的增加而急劇上升。

3.4.2 添加一個新方法

如果不能做到不改變接口,那我們至少要做到改變後的接口與原有接口向後相容。保證向後相容性的一種解決辦法,就是不改變原有方法,而是添加一個新的方法。過程如下:

首先,為

UserService

接口添加一個新的方法

__rpc_create

。這個方法名雖然看起來有些奇怪,但卻有兩點好處:第一、不會和已有方法重名,因為 Java 命名規範不建議使用這樣的辨別符來為方法命名;第二、在原有方法前加上

__rpc_

字首,能夠做到與原有方法對應,便于閱讀和了解。示例如下:

public interface UserService {
  ...      

// 保持原有方法不變

void create(User user);

// 添加一個方法,新方法需要有傳回值

User __rpc_create(User user);

然後,在實作類中實作這個新方法:

@Service("userService")
public class DefaultUserService implements UserService {
  ...      

// 保持原有方法實作不變

@Override

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

// 添加新方法的實作

public User __rpc_create(User user) {

// 調用原來的方法
this.create(user);
// 傳回變更後的資料
return user;           

有一點需要展開解釋:在

__rpc_create

方法中,因為

user

參數是以引用的方式傳遞給

create

方法的,是以

create

方法對參數所做的修改是能夠被

__rpc_create

方法擷取到的。這以後就與前面回傳的邏輯是相同的了。

第三,在服務引用端添加本地存根(有關本地存根的概念及用法,請參考官方文檔)。

需要在

hello-service-reference

子產品中添加一個類

UserServiceStub

,内容如下:

public class UserServiceStub implements UserService {
  private UserService userService;      

public UserServiceStub(UserService userService) {

this.userService = userService;           
return this.userService.getById(id);           
User newUser = this.__rpc_create(user);
user.setCreatedTime(newUser.getCreatedTime());           
return this.userService.__rpc_create(user);           

該類型即為本地存根。簡單來說,就是在調用方調用本地代理的方法之前,會先去調用本地存根中相應的方法,是以本地存根與服務提供方和服務引用方需要實作同樣的接口。本地存根中的構造函數是必須的,且方法簽名也是被約定好的——需要傳入本地代理作為參數。其中

getById

__rpc_create

方法都是直接調用了本地代理中的方法,不必過多關注,重點來說說

create

方法。首先,

create

調用了本地存根中的

__rpc_create

方法,這個方法透過本地代理通路到了服務提供方的相應方法,并成功接收了傳回值

newUser

,這個傳回值是包含修改後的

createdTime

字段的,于是我們要做的事情就是從

newUser

對象裡面擷取到

createdTime

字段的值,并設定給

user

參數,以達到産生副作用的效果。此時

user

參數會帶着新設定的

createdTime

的值,将其“傳遞”給

create

方法的調用方。

最後,在

dubbo-references.xml

檔案中修改一處配置,以啟用該本地存根:

interface="net.tangrui.demo.dubbo.hello.service.UserService"
  version="1.0"
  stub="net.tangrui.demo.dubbo.hello.service.stub.UserServiceStub" />
      

鑒于本地存根的工作機制,我們是不需要修改調用方

hello-web

子產品中的任何代碼及配置的。編譯、運作并測試,同樣可以達到我們想要的效果。

使用 Dubbo 對遺留單體系統進行微服務改造編譯、打包和安裝在項目根目錄下執行指令運作在 hello-web 目錄下執行指令

這種實作方式會比第一種方式改進不少,但也有緻命弱點:

保持了接口的向後相容性

引入本地存根,無需修改調用方代碼

通過配置可以實作改造結果的可插拔

實作複雜,尤其是本地存根的實作,如果遺留系統的代碼對傳入參數裡的内容進行了無節制的修改的話,那麼重制該副作用效果是非常耗時且容易出錯的

難以了解

四、總結

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

微服務在當下已經不是什麼新鮮的技術了,但曆史包袱依然是限制其發展的重要因素,希望這篇文章能帶給大家一點啟發,在接下來的工作中更好的擁抱微服務帶來的變革。