摘要: 在 2016 年 11 月份的《技術雷達》中,ThoughtWorks 給予了微服務很高的評價。同時,也有愈來愈多的組織將實施微服務做爲架構演進的一個必選方向。只不過在擁有衆多遺留系統的組織內,將曾經的單體系統拆分爲微服務並非一件容易的事情。java
Credit: Justin Kenneth Rowley. You can find the original photo at flickr.web
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.spring
在 2016 年 11 月份的《技術雷達》中,ThoughtWorks 給予了微服務很高的評價。同時,也有愈來愈多的組織將實施微服務做爲架構演進的一個必選方向。只不過在擁有衆多遺留系統的組織內,將曾經的單體系統拆分爲微服務並非一件容易的事情。本文將從對遺留系統進行微服務改造的原則要求出發,探討如何使用 Dubbo 框架實現單體系統向微服務的遷移。編程
1、原則要求api
想要對標準三層架構的單體系統進行微服務改造——簡言之——就是將曾經單一進程內服務之間的本地調用改造爲跨進程的分佈式調用。這雖然不是微服務改造的所有內容,但卻直接決定了改造先後的系統可否保持相同的業務能力,以及改形成本的多少。網絡
1.1 適合的框架架構
在微服務領域,雖然技術棧衆多,但無非 RPC 與 RESTful 兩個流派,這其中最具影響力的表明當屬 Dubbo 與 Spring Cloud 了 。他們擁有類似的能力,卻有着大相徑庭的實現方式——本文並非想要對微服務框架的選型過程進行深刻剖析,也不想對這兩種框架的孰優孰劣進行全面比較——本章所提到的所有這些原則要求都是超越具體實現的,其之於任何微服務框架都應該是適用的。讀者朋友們大能夠把本文中的 Dubbo 所有替換爲 Spring Cloud,而並不會對最終結果形成任何影響,惟一須要改變的僅僅是實現的細節過程而已。所以,不管最後抉擇如何,都是無所謂對錯的,關鍵在於:要選擇符合組織當下現狀的最適合的那一個。app
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、模擬一個單體系統
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() {
}
}
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 {
}
@Override
public User getById(String id) {
return STORE.get(id);
}
@Override
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);
void create(User user);
}
與 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;
@Override
public User getById(String id) {
}
@Override
public void create(User user) {
}
}
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;
@Override
public String sayHello(String userId) {
}
}
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;
@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
類型是一個標準的 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
$ mvn spring-boot:run
測試結果以下,注意每次輸出括號裏面的日期時間,它們都應該是有值的。
再返回 hello-web 系統的控制檯,查看一下日誌輸出,時間應該與上面是同樣的。
3、動手改造
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。
3.2.2 添加 POM 文件
有關 Maven 的使用與配置也不是本文關注的重點,可是這個模塊用到了一些 Maven 插件,在此對這些插件的功能和做用進行一下描述。
3.2.3添加 assembly.xml 文件
Assembly 插件的主要功能是對項目從新打包,以便自定義打包方式和內容。對本項目而言,須要生成一個壓縮包,裏面包含全部運行該服務所須要的 jar 包、配置文件和啓動腳本等。Assembly 插件須要 assembly.xml
文件來描述具體的打包過程,該文件須要擺放在 src/main/assembly 目錄下。有關 assembly.xml 文件的具體配置方法,請參考官方文檔。
3.2.4 添加 logback.xml 文件
因爲在 POM 文件中指定了使用 logback 做爲日誌輸出組件,所以還須要在 logback.xml
文件中對其進行配置。該文件須要擺放在 src/main/resources 目錄下,有關該配置文件的具體內容請參見代碼倉庫,有關配置的詳細解釋,請參考官方文檔。
3.2.5 打包
因爲已經在 POM 文件中定義了打包的相關配置,所以直接在 hello-service-provider
目錄下運行如下命令便可:
$ mvn clean package
成功執行之後,會在其 target 目錄下生成一個名爲
hello-service-provider-0.1.0-SNAPSHOT-assembly.tar.gz
的壓縮包,裏面的內容如圖所示:
3.2.6 運行
如此配置完成之後,就可使用以下命令來啓動服務:
$ MAVEN_OPTS="-Djava.net.preferIPv4Stack=true" mvn exec:java
注:在 macOS 系統裏,使用 multicast 機制進行服務註冊與發現,須要添加-Djava.net.preferIPv4Stack=true
參數,不然會拋出異常。
可使用以下命令來判斷服務是否正常運行:
$ netstat -antl | grep 20880
若是有相似以下的信息輸出,則說明運行正常。
若是是在正式環境運行,就須要將上一步生成的壓縮包解壓,而後運行 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/ 目錄下。文件的內容很是簡單明瞭:
但不一樣於 hello-service-provider 模塊的一點在於,該模塊只須要打包成一個 jar 便可,POM 文件內容以下:
總結一下,咱們曾經的遺留系統分爲三個模塊 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,再經過 hellocore 間接依賴 hello-api,而如今咱們須要將其改變爲直接依賴 hello-service-reference 模塊,再經過 hello-service-reference 模塊間接依賴 hello-api。改造先後的依賴關係分別爲:
3.3.2 啓動服務
由於是測試環境,只須要執行如下命令便可,但在進行本操做以前,須要先啓動 hello-service-provider
服務。
$ MAVEN_OPTS="-Djava.net.preferIPv4Stack=true" mvn spring-boot:run
Oops!系統並不能像指望的那樣正常運行,會拋出以下異常:
意思是說 net.tangrui.demo.dubbo.hello.web.Controller
這個類的 helloService
字段須要一個類型爲 net.tangrui.demo.dubbo.hello.service.HelloService
的 Bean,可是沒有找到。相關代碼片斷以下:
@RestController
Public class Controller {
@Autowired
private HelloService helloService;
@Autowired
private UserService userService;
...
}
顯然,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
就是咱們須要的。
這時再從新嘗試啓動,就一切正常了。
可是,咱們如何驗證結果確實是從 hello-service-provider 服務過來的呢?這時就須要用到 DefaultUserService裏面的那幾行日誌輸出了,回到 hello-service-provider 服務的控制檯,可以看到相似這樣的輸出:
如此即可以確信系統的拆分是被成功實現了。再試試建立用戶的接口:
$ curl -X POST 'http://127.0.0.1:8080/create?userId=huckleberry&name=Huckleberry%20Finn'
等等,什麼!括號裏面的建立時間爲何是 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 字段確實是有值的。
那麼該如何讓這個反作用效果也可以被處於另一個虛擬機中的 hello-web
系統感知到呢,方法只有一種,就是將變動後的數據回傳。
3.4.1 爲方法添加返回值
這是最容易想到的一種實現方式,簡單的說就是修改服務接口,將變動後的數據返回。
首先,修改 UserService
接口的 create
方法,添加返回值:
public interface UserService {
...
// 爲方法添加返回值
User create(User user);
}
而後,修改實現類中相應的方法,將變動後的 user
對象返回:
@Service("userService")
public class DefaultUserService implements UserService {
...
@Override
public User create(User user) {
}
}
最後,修改調用方實現,接收返回值:
@RestController
public class Controller {
...
@RequestMapping(path = "/create", method = RequestMethod.POST)
public String createUser(@RequestParam("userId") String userId, @RequestParam("name") String name) {
}
}
編譯、運行並測試(以下圖),正如咱們所指望的,括號中的建立時間又回來了。其工做原理與本節開始時所描述的是同樣的,只是方向相反而已。在此再也不詳細展開,留給你們自行思考。
這種修改方式有以下一些優缺點:
方法簡單,容易理解
改變了系統接口,且改變後的接口與原有接口不兼容(違背了 1.4 節關於「保持原有接口不變或向後兼容」原則的要求)
由此也不可避免的形成了對遺留系統內部代碼的修改(違背了 1.7 節關於「儘可能少改動(最好不改動)遺留系統的內部代碼」原則的要求)
修改方式不可插拔(違背了 1.9 節「改造結果可插拔」原則的要求)
因而可知,這種改法雖然簡單,倒是利大於弊的,除非咱們可以徹底掌控整個系統,不然這種修改方式的風險會隨着系統複雜性的增長而急劇上升。
3.4.2 添加一個新方法
若是不能作到不改變接口,那咱們至少要作到改變後的接口與原有接口向後兼容。保證向後兼容性的一種解決辦法,就是不改變原有方法,而是添加一個新的方法。過程以下:
首先,爲 UserService
接口添加一個新的方法 __rpc_create
。這個方法名雖然看起來有些奇怪,但卻有兩點好處:第1、不會和已有方法重名,由於 Java 命名規範不建議使用這樣的標識符來爲方法命名;第2、在原有方法前加上 __rpc_
前綴,可以作到與原有方法對應,便於閱讀和理解。示例以下:
public interface UserService {
...
// 保持原有方法不變
void create(User user);
// 添加一個方法,新方法須要有返回值
User __rpc_create(User user);
}
而後,在實現類中實現這個新方法:
@Service("userService")
public class DefaultUserService implements UserService {
...
// 保持原有方法實現不變
@Override
public void create(User user) {
}
// 添加新方法的實現
@Override
public User __rpc_create(User 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;
}
@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);
}
}
該類型即爲本地存根。簡單來講,就是在調用方調用本地代理的方法以前,會先去調用本地存根中相應的方法,所以本地存根與服務提供方和服務引用方須要實現一樣的接口。本地存根中的構造函數是必須的,且方法簽名也是被約定好的——須要傳入本地代理做爲參數。其中 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
模塊中的任何代碼及配置的。編譯、運行並測試,一樣能夠達到咱們想要的效果。
這種實現方式會比第一種方式改進很多,但也有致命弱點:
保持了接口的向後兼容性
引入本地存根,無需修改調用方代碼
經過配置能夠實現改造結果的可插拔
實現複雜,尤爲是本地存根的實現,若是遺留系統的代碼對傳入參數裏的內容進行了無節制的修改的話,那麼重現該反作用效果是很是耗時且容易出錯的
難以理解
4、總結
至此,將遺留系統改造爲微服務系統的任務就大功告成了,並且基本上知足了文章最開始提出來的十點改造原則與要求(此處應給本身一些掌聲),不知道是否對你們有所幫助?雖然示例項目是爲了敘述要求而量身定製的,但文章中提到的種種理念與方法卻實實在在是從實踐中摸索和總結出來的——踩過的坑,遇到的問題,解決的思路以及改造的難點等都一一呈現給了你們。
微服務在當下已經不是什麼新鮮的技術了,但歷史包袱依然是限制其發展的重要因素,但願這篇文章能帶給你們一點啓發,在接下來的工做中更好的擁抱微服務帶來的變革。