现在已经进入了全民微服务时代。人们谈论如何基于微服务架构来构建应用系统,或者如何用微服务架构思想把已有的单体系统微服务化。不过,这篇文章不是要讨论微服务,而是一个实际项目的重构案例学习。重构所涉及的范围也很广泛,小到代码的重构,大到整个系统的重构。这里要分享的是一个接口分离的例子,类似于把某些模块从一个单体系统里抽离出来,成为公共服务的过程,说白了还是有点微服务化的意思。
我们假设读者已经知道如何使用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的应用越来越多,可能需要对它进行负载均衡。关于负载均衡,可以参考另一篇文章。