文档章节

Case Study : 重构与RPC

半包咖啡豆
 半包咖啡豆
发布于 2016/09/09 17:03
字数 1835
阅读 291
收藏 1

        现在已经进入了全民微服务时代。人们谈论如何基于微服务架构来构建应用系统,或者如何用微服务架构思想把已有的单体系统微服务化。不过,这篇文章不是要讨论微服务,而是一个实际项目的重构案例学习。重构所涉及的范围也很广泛,小到代码的重构,大到整个系统的重构。这里要分享的是一个接口分离的例子,类似于把某些模块从一个单体系统里抽离出来,成为公共服务的过程,说白了还是有点微服务化的意思。

        我们假设读者已经知道如何使用Spring Boot,Spring Data MongoDB以及一些RPC框架,并且知道Java动态代理基本原理。

 

单体应用

        我们有一个叫GW的应用,应用把数据存储在MongoDB里。GW使用了Spring Data MongoDB访问数据库,其中的一个接口叫UserRepository,这个接口包含了GW访问User相关数据的所有操作。不过它也包含两部分,一部分是Spring Data MongoDB提供的标准接口MongoRepository,它提供标准的CRUD操作,框架提供了实现,所以我们不需要写任何实现代码;另一部分UserRepositoryCustom是通过使用MongoTemplate来实现的,需要我们自己写代码。

 

应用依赖

        如果只是这样,GW应用可以运行地很好,没有什么问题。但是业务总是在不断地变化,很多意想不到的情况随时在等待着我们。后来有一个独立于GW之外的应用,叫FC,它也需要访问User相关数据。我们不可能为FC再去实现一个一模一样的UserRepository,而是想着怎么重用已有的接口。于是出现了下图所示的架构:

        一个系统要重用另一个系统的功能,只能通过远程调用来实现,我们选择了RPC。我们在GW系统里挖了一个“口子”,为FC提供了一个远程接口(图中橙色部分)。FC通过这个接口来使用GW的UserRepository功能,这样,GW和FC两个系统都可以访问User相关的数据。

        上面的架构虽然暂时解决了在系统间共享接口的问题,但从长远来看,它存在很多问题。首先,随着应用数量的增加,GW可能需要为其它应用开出更多的“口子”来满足重用现有功能的需要,不仅被搞得“千疮百孔”,整个应用也越来越臃肿。再则,GW除了要承担自身的工作量,还要处理来自其它应用的请求,压力会越来越大。所以我们需要一个更好的方案。

 

应用剥离

        既然GW和FC都依赖UserRepository,那么直接把它从GW分离出来也许是个更好的主意。剥离出来以后,不仅仅GW和FC可以调用它,以后新增的应用也可以调用。如果有性能的需要,还可以对它进行横向扩展。

        这样,UserRepository和它的实现就从GW应用里分离了出来,成为一个独立的服务,我们姑且叫它Service。Service对外提供UserRepository接口,这个接口跟原来一模一样,因为只有这样才有可能做到不对GW和FC已有代码做太多的改动来完成重构。如果说要完成这个重构需要大动干戈,那么我们就得想想是不是还有其它更好的方案了。所以,重构其中的一个目标就是要求尽量不改动调用UserRepository的代码。

 

如何实现

        重构目标明确了以后,接下来就是实现了。

        首先,我们把UserRepository接口跟实现代码从GW挪到Service里,要保证UserRepository的接口不要做任何改动。在Service里,UserRepository接口和实现被分为两个Maven模块,因为接口部分会被打成jar包作为调用端RPC的接口依赖。当然,Service的实现部分也需要依赖这个jar包。

        接下来,我们选择一种RPC框架,把UserRepository接口服务暴露出来。我们可以用dubbo,hessian,或者其它任意一种可以与Spring很好集成的RPC框架。

        最后,对GW和FC稍作修改,把UserRepository接口的实现配置指向Service。具体的配置要根据所选择的RPC框架而定。

 

动态代理

        当然,如果讨论得再细一点,这里其实还有一个地方值得一说。

        之前我们提过,UserRepository其实是由两部分组成,一部分是Spring Data MongoDB实现的标准CRUD操作,还有一部分是自定义的操作。在剥离之前,所有对UserRepository的调用都是在本地进行的,Spring框架帮我们隐藏了很多调用细节。现在接口被放到了Service里,成为远程接口。从实现角度来看,我们要怎么在Service里来实现这个接口呢?

        对UserRepository的实现也仍然分为两个部分,对于MongoRepository部分,仍然使用Spring Data MongoDB的支持,对于UserRepositoryCustom部分,还是使用MongoTemplate来实现。问题是,如何把对UserRepository接口的调用同这两个实现一一对应起来?比如调用端可能调用的是MongoRepository部分的接口,或者UserRepositoryCustom部分的接口,我们如何把这些调用准确地委托给正确的实例?

        简单点说,我们可以为UserRepository创建一个实现类,实现每一个方法,其实最终是想把每一个方法调用委托给相应的实例。但是这个接口可能有上百个方法,我们手动去一个一个实现会是一个很枯燥无味的工作。在这里,使用Java动态代理会帮我们省掉很多工作。下面将给出示例代码。

        首先是为UserRepository创建Proxy,而不是实现类。

(UserRepository)Proxy.newProxyInstance(UserRepository.class.getClassLoader(),new Class<?>[]{UserRepository.class}, new UserRepositoryInvocationHandler(mongoRepo,customRepo));

        这个代码片段为UserRepository接口创建了一个Proxy,我们可以把它认为就是接口的一个实现。其中UserRepositoryInvocationHandler负责把方法调用委托给正确的实例。

private StandardUserRepository standard;
private CustomizedUserRepository customized;
private ConcurrentMap<Method, Object> cache = new ConcurrentHashMap<>();

public UserRepositoryInvocationHandler(StandardUserRepository standard,CustomizedUserRepository customized) {
    this.standard = standard;
    this.customized = customized;
}

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    Object target = cache.get(method);
    if (target == null) {
    try {
        Object result = method.invoke(standard, args);
        cache.put(method, standard);
        return result;
    } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
        Object result = method.invoke(customized, args);
        cache.put(method, customized);
        return result;
    }
    }
    return method.invoke(target, args);
}

        standard和customized分别是UserRepository标准和自定义部分的实现实例,在invoke方法里,UserRepository的方法调用分别被应用到这两个实例上。方法到实例的映射被缓存起来,这样不用每次都去判断方法调用该应用到哪个实例上。

 

负载均衡

        随着调用Service的应用越来越多,可能需要对它进行负载均衡。关于负载均衡,可以参考另一篇文章

© 著作权归作者所有

半包咖啡豆
粉丝 5
博文 14
码字总数 44502
作品 0
浦东
架构师
私信 提问
Thrift、IPC、RPC的概念

版权声明:有些文章写的很随意,那是仅作为个人记录的文章,建议直接关掉,多看一秒亏一秒 https://blog.csdn.net/qq_36523667/article/details/86017457 看到部门整个系统的架构图,内部系统...

徐风来
01/07
0
0
Go 重构 PHP 项目的一个神器 jsoniter

Go 和 PHP 通信是痛苦的,PHP 这个神奇的 json ,各种奇奇怪怪的 json 都能生成出来,如果一开始就奇怪直接用interface也就算了,最可怕那种是类型会变。 在用 Go 重构 PHP 项目的时候,两个...

anoty
2018/11/06
258
0
zbus-6.2.0 发布,轻量级 MQ,RPC 服务总线

zbus-6.2.0 发布,整个项目模块化重构,改进内容: MQ默认直接采用持久化。MQ速度在SSD磁盘上可达200M+/s,数十亿级消息堆积测试(100G+磁盘) RPC支持分为直接RPC与基于MQ的RPC,直接RPC性能...

少帮主
2015/09/21
3.6K
27
Kiwi TCMS 5.1 发布,开源全功能测试用例管理系统

Kiwi TCMS 5.1 已发布,Kiwi TCMS 是一个集测试计划、测试运行和测试用例于一身的管理系统,用 Python 和 Django 编写。它具有许多强大的功能,如 Bugzilla 和 JIRA 集成,快速测试计划和运行...

局长
2018/08/01
1K
0
Kiwi TCMS 6.4 发布,全功能测试用例管理系统

Kiwi TCMS 6.4 已发布,Kiwi TCMS 是一个集测试计划、测试运行和测试用例于一身的管理系统,用 Python 和 Django 编写。它具有许多强大的功能,如 Bugzilla 和 JIRA 集成,快速测试计划和运行...

程六金
01/09
1K
1

没有更多内容

加载失败,请刷新页面

加载更多

Qt编写自定义控件69-代码行数统计

一、前言 代码行数统计主要用来统计项目中的所有文件的代码行数,其中包括空行、注释行、代码行,可以指定过滤拓展名,比如只想统计.cpp的文件,也可以指定文件或者指定目录进行统计。写完这...

飞扬青云
12分钟前
2
0
驰骋工作流引擎-ccflow关于 “ 是否自动计算未来的处理人”的功能变更

关键字:流程未来节点处理人 工作流快速开发平台 工作流流设计 业务流程管理 asp.net 开源工作流 业务背景:一个流程在启动起来后,是可以对一些节点计算出来处理人是谁,流程的走向。对于另...

孟娟
28分钟前
4
0
IT兄弟连 HTML5教程 HTML5表单 HTML表单设计1

表单是PHP程序中最常使用的收集站点访问者信息的数据输入界面。通过表单浏览器获取用户的输入数据,并传送给Web服务器的脚本程序中,以各种不同的方式进行处理。在表单中提供了多种输入方式,...

老码农的一亩三分地
29分钟前
2
0
武者Vue

本文转载于:专业的前端网站➼武者Vue 1 - Introduction2 - The Vue Instance3 - Data & Methods4 - Data Binding5 - Events6 - Event Modifiers7 - Keyboard Events8 - Two-Way Data......

前端老手
35分钟前
4
0
uni app 零基础小白到项目实战

$emit 子组件传给父组件$ref 父组件操作子组件 公用模板 uni-app全局变量的几种实现方法 const websiteUrl = 'http'const now = Date.now || function() { return new Date().getTime......

达达前端小酒馆
45分钟前
7
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部