使用 Dubbo 对遗留单体系统进行微服务改造

摘要: 在 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 类型的对象 userRepositorygetUserById 方法根据 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 类型的对象 userServicesayHello 方法根据 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-apihello-core 和 hello-web,是用 Maven 来管理的。到目前为止所涉及到的 POM 文件都比较简单,为了节约篇幅,就不在此一一列出了,感兴趣的朋友能够到项目的 Github 仓库上自行研究。

hello-dubbo 项目的打包和运行都很是直接:

 

编译、打包和安装

在项目根目录下执行命令

$ mvn clean install

运行

在 hello-web 目录下执行命令

$ mvn spring-boot:run

 

测试结果以下,注意每次输出括号里面的日期时间,它们都应该是有值的。

再返回 hello-web 系统的控制台,查看一下日志输出,时间应该与上面是同样的。

3、动手改造

3.1 改造目标

上一章,咱们已经成功构建了一个模拟系统,该系统是一个单体系统,对外提供了两个 RESTful 接口。本章要达到的目标是将该单体系统拆分为两个独立运行的微服务系统。如 2.2 节所述,进行模块化拆分是实施微服务改造的重要一步,由于在接下来的描述中会暗含一个约定:hello-webhello-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-reference 模块来远程调用 hello-service-provider 系统才能完成业务。而 hello-service-provider 系统则须要暴露可供 hello-service-reference 模块调用的远程接口,并实现 Service 及 DAO 接口定义的具体业务逻辑。

本章节就是要重点介绍 hello-service-provider 和 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、总结

至此,将遗留系统改造为微服务系统的任务就大功告成了,并且基本上知足了文章最开始提出来的十点改造原则与要求(此处应给本身一些掌声),不知道是否对你们有所帮助?虽然示例项目是为了叙述要求而量身定制的,但文章中提到的种种理念与方法却实实在在是从实践中摸索和总结出来的——踩过的坑,遇到的问题,解决的思路以及改造的难点等都一一呈现给了你们。

微服务在当下已经不是什么新鲜的技术了,但历史包袱依然是限制其发展的重要因素,但愿这篇文章能带给你们一点启发,在接下来的工做中更好的拥抱微服务带来的变革。

原文连接

相关文章
相关标签/搜索