1. 背景

最近在做数据库(MySQL)方面的升级改造。现状是数据库同时被多个应用直连,存在了一些问题:

  1. 有大量的重复代码,维护成本较高,也不优雅;
  2. 出现SQL语句质量的问题无法很快定位到是哪个应用导致的;
  3. 数据库调用方过于分散,不便于统一控制,比如部分业务数据的读写、屏蔽等;
  4. 业务的发展,有的表数据量已经到了一定的规模,几百万到几千万不等,数据库存储拆分是必须要进行的事情。

解决问题的方式很简单,就是把各应用中与此业务相关的dao层抽象成为一个单独应用(以下称为internal-rpc-app),进行统一管理。

2. 具体实现

具体的业务应用与internal-rpc-app的内部通信使用公司内部具有服务治理功能的RPC框架JSF,JSF是一款稳定高效的框架,它对服务治理的粒度是接口,接口通过Spring做服务的发布和调用配置。每个接口对应一个数据表的CURD及特殊业务。

2.1 标准版本 1.0

2.1.1. 接口注册申请

注册接口: com.jd.xx.BizRpcService1
注册接口: com.jd.xx.BizRpcService2
注册接口: com.jd.xx.BizRpcService3
......
注册接口: com.jd.xx.BizRpcServiceN

2.1.2. 服务提供方配置

<jsf:provider id="rpc1" interface="com.jd.xx.BizRpcService1"/>
<jsf:provider id="rpc2" interface="com.jd.xx.BizRpcService2"/>
<jsf:provider id="rpc3" interface="com.jd.xx.BizRpcService3"/>
......
<jsf:provider id="rpcN" interface="com.jd.xx.BizRpcServiceN"/>

2.1.3. 客户端调用方配置

<jsf:consumer id="rpc1" interface="com.jd.xx.BizRpcService1"/>
<jsf:consumer id="rpc2" interface="com.jd.xx.BizRpcService2"/>
<jsf:consumer id="rpc3" interface="com.jd.xx.BizRpcService3"/>
......
<jsf:consumer id="rpcN" interface="com.jd.xx.BizRpcServiceN"/>

2.1.4 小结

此版本实现了我们的最初的目的,但是有一个不太好的地方,就是配置的工作量太高,前面有说到JSF框架的治理维度是接口。这也意味着,每次新增接口都要提交申请操作,同时要在consumer和provider做相对应的配置,几个还好,如果数据表有几十个上百个,重复的工作量就很大。同时内部接口也不需要做太细粒度的服务治理。于是有了第二版,主要目的是简化大量重复配置。

2.2 优化调用体验版本 2.0

2.2.1 内部实现 - 客户端

首先定义调用API:

BizRpcService1 bizRpcService1 = InternalPrxoyServices.BizRpcService1;
bizRpcService1.method1(param1, param2);

InternalPrxoyServices关键代码:

public class InternalPrxoyServices {
BizProxyRpcService1 BizRpcService1 = new BizProxyRpcService1();
BizProxyRpcService2 BizRpcService2 = new BizProxyRpcService2();
BizProxyRpcService2 BizRpcService3 = new BizProxyRpcService3();
......
BizProxyRpcServiceN BizRpcServiceN = new BizProxyRpcServiceN();
}

BizProxyRpcService1关键代码:

public class BizProxyRpcService1 extends BaseProxyService implements BizRpcService1 {
@Override
public String method1(String param1, String param2) throws InternalRpcException {
return super.execute(param1, param2);
}
}

BaseProxyService关键代码:

abstract class BaseProxyService implements InternalRpcService {
private InternalRpcService internalRpcService; private Map<String, String> interfaceMap = new HashMap<String, String>(); BaseProxyService() {
// 已经被注入到一个静态变量中,此处直接获取
internalRpcService = InternalRpcServiceHolder.getInternalRpcService();
} @Override
<T> T execute(Object... objects) throws InternalRpcException {
StackTraceElement[] stackTraceElements = Thread.currentThread().getStackTrace();
StackTraceElement callerStackTraceElement = stackTraceElements[2];
// 要取代理类实现到具体接口名称和方法名称以此定位具体到逻辑,框架不支持重名,所以重名问题不考虑
String interfaceName = getInterfaceName(callerStackTraceElement.getClassName());
String methodName = callerStackTraceElement.getMethodName();
return (T) internalRpcService.execute(interfaceName, methodName, objects);
}
}

InternalRpcService接口定义:

public interface InternalRpcService {
Object execute(String serviceName, String methodName, Object... objects) throws InternalRpcException;
}

2.2.2 内部实现 - 服务端

服务端实现比较简单,直接根据接口传过来当serviceName、methodName、objects即可定位到具体service到方法,直接执行即可。

2.2.3 小结

到这里,我们通过静态代理实现了具体的目标,通过实现具体的接口类。我们不再需要定义过多的配置了,客户端调用也变得简单明了。

那么,结束了吗?并没有,在解决了大量配置的问题的同时,因为要写大量的代理类,又引入了新的工作量。

2.3 最终版本 3.0

在这一版,我们很自然的引入了动态生成代理。

2.3.1 客户端具体实现

因为引入了动态代理,所以要重新改写InternalPrxoyServices:

public class InternalPrxoyServices {
BizRpcService1 BizRpcService1 = ProxyServiceFactory.create(BizRpcService1.class);
BizRpcService2 BizRpcService2 = ProxyServiceFactory.create(BizRpcService2.class);
BizRpcService3 BizRpcService3 = ProxyServiceFactory.create(BizRpcService3.class);
......
BizRpcServiceN BizRpcServiceN = ProxyServiceFactory.create(BizRpcServiceN.class);
}

ProxyServiceFactory关键代码:

public class ProxyServiceFactory {

    public static <T> T create(Class<T> internalInterface) {
final InternalRpcProxy<T> internalRpcProxy = new InternalRpcProxy<T>(internalInterface);
return (T) Proxy.newProxyInstance(internalInterface.getClassLoader(), new Class[] { internalInterface }, internalRpcProxy);
}
}

InternalRpcProxy关键代码:

class InternalRpcProxy<T> implements InvocationHandler, Serializable {
private final Class<T> internalRpcInterface;
InternalRpcProxy(Class<T> internalRpcInterface) {
this.internalRpcInterface = internalRpcInterface;
}
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
return InternalRpcServiceHolder.internalRpcService.execute(internalRpcInterface.getCanonicalName(), method.getName(), args);
}
}

2.4 小结

至此,已经解决了2.2.3提到的大量创建代理类的问题。

当然我们还做了很多文章中没有提及的事情,比如:

  • 通过声明哪些接口可以走动态代理
  • 方法重名、客户端接口合法性等校验
  • 将method存入缓存

3. 总结

通过建立独立的应用,解决了前面数据库被多应用读写所产生的问题,通过开发了统一接口解决了服务端和客户端配置过多的问题。代码经过一步步抽象后,最终发现实现了一个简单的RPC雏形,只不过通信层是基于公司的JSF框架。

这引发了一些框架方面的思考:

  1. 是否应该去掉人工审批类似的流程?
  2. 是否应该允许更灵活的发布服务,比如:根据注解自动扫描发布服务,根据注解自动获取服务。

这不是技术问题,而是怎么权衡的问题。

记一次Java动态代理实践【首发自高可用架构公众号】的更多相关文章

  1. [置顶] 来自 Google 的高可用架构理念与实践

    转自:   https://mp.weixin.qq.com/s?__biz=MzAwMDU1MTE1OQ==&mid=402738153&idx=1&sn=af5e76aad ...

  2. Java 动态代理机制分析及扩展

    Java 动态代理机制分析及扩展,第 1 部分 王 忠平, 软件工程师, IBM 何 平, 软件工程师, IBM 简介: 本文通过分析 Java 动态代理的机制和特点,解读动态代理类的源代码,并且模拟 ...

  3. [转]Java 动态代理机制分析及扩展

    引言 Java 动态代理机制的出现,使得 Java 开发人员不用手工编写代理类,只要简单地指定一组接口及委托类对象,便能动态地获得代理类.代理类会负责将所有的方法调用分派到委托对象上反射执行,在分派执 ...

  4. Java 动态代理机制分析及扩展--转

    http://www.ibm.com/developerworks/cn/java/j-lo-proxy1/#icomments http://www.ibm.com/developerworks/c ...

  5. Java 动态代理机制分析及扩展,第 1 部分

    Java 动态代理机制分析及扩展,第 1 部分 http://www.ibm.com/developerworks/cn/java/j-lo-proxy1/ 本文通过分析 Java 动态代理的机制和特 ...

  6. 一文读懂Java动态代理

    作者 :潘潘 日期 :2020-11-22 事实上,对于很多Java编程人员来说,可能只需要达到从入门到上手的编程水准,就能很好的完成大部分研发工作.除非自己强主动获取,或者工作倒逼你学习,否则我们好 ...

  7. JAVA动态代理的全面深层理解

    Java 动态代理机制的出现,使得 Java 开发人员不用手工编写代理类,只要简单地指定一组接口及委托类对象,便能动态地获得代理类.代理类会负责将所有的方法调用分派到委托对象上反射执行,在分派执行的过 ...

  8. Java 动态代理作用是什么?

    Java 动态代理作用是什么?   1 条评论 分享   默认排序按时间排序 19 个回答 133赞同反对,不会显示你的姓名 Intopass 程序员,近期沉迷于动漫ING 133 人赞同 ① 首先你 ...

  9. Java 动态代理

    被代理的接口特点: 1. 不能有重复的接口,以避免动态代理类代码生成时的编译错误. 2. 这些接口对于类装载器必须可见,否则类装载器将无法链接它们,将会导致类定义失败. 3. 需被代理的所有非 pub ...

随机推荐

  1. 一款回到顶部的 jQuery 插件,支持 Div 中的滚动条回到顶部

    前言 今天在网上搜索“回到顶部”的 jQuery 插件,网上有很多,但是大部分都不支持让 Div 中的滚动条回到顶部.于是乎,不放弃,自己参考 Github 上的一个 jQuery 插件,经过自己的修 ...

  2. Linux 系统假死的解决方案

    Linux 系统有时因为软件不兼容或未知 bug,导致系统假死.比如我的 Ubuntu 14.04 最近使用 vscode 时偶尔会导致系统假死,即鼠标可以移动,但点击无反应. 此时可试试如下解决方案 ...

  3. Core官方DI解析(5)-ServiceProviderEngine

    最后来看看前面一直说的Engine(工作引擎),工作引擎接口是IServiceProviderEngine在ServiceProvider的构造函数中看到了根据指定的Mode创建了不同的实现类,下面先 ...

  4. 特殊需求:EF 6.x如何比较TimeSpan格式的字符串?EF Core实现方式是否和EF 6.x等同?

    前言 我们知道C#中的TimeSpan对应SQL Server数据库中的Time类型,但是如果因为特殊需求数据库存储的不是Time类型,而是作为字符串,那么我们如何在查询数据时对数据库所存储的字符串类 ...

  5. url 的正则表达式:path-to-regexp

    概述 该工具库用来处理 url 中地址与参数,能够很方便得到我们想要的数据. js 中有 RegExp 方法做正则表达式校验,而 path-to-regexp 可以看成是 url 字符串的正则表达式. ...

  6. 论学好Linux系统的超级重要性

    不知道各位在日常的工作生活中有没有接触过“rm -rf /*”这个命令,因为这个命令搞出来的事情可还不少呢!前段时间就在一个群里看到了有个小伙子,老板让他去维护一下服务器,这小伙也不太懂,就问群里的大 ...

  7. form单选框

    form中的单选框: var resultStartRadio = new Ext.form.RadioGroup({ id : 'resultStartRadio', name :"for ...

  8. C#需要在项目程序生成前后执行相关的事件

    分享4: 需求:需要在项目程序生成前后执行相关的事件,比如:需要将某个文件拷贝到bin\Debug中,或者创建某文件夹等. 分析:我们可利用项目属性(选择项目右键,选择属性)中的“生成事件”预定义相关 ...

  9. svg-sprite使用

    chainWebpack(config) { config.module .rule('svg') .exclude.add(path.resolve(__dirname,'src/assets/ic ...

  10. Asp.Net Core中DI的知识总结

    在asp.net core中DI的概念是由这几部分组成的: IServiceCollection,保存IServiceDescriptor实例的列表 IServiceProvider,只有一个方法Ge ...