zoukankan      html  css  js  c++  java
  • 记一次Java动态代理实践【首发自高可用架构公众号】

    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. 是否应该允许更灵活的发布服务,比如:根据注解自动扫描发布服务,根据注解自动获取服务。

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

  • 相关阅读:
    高地址,低地址:
    大小端
    大小端
    linux下C语言编程解决warning : incompatible implicit declaration of built-in function问题
    给char赋16进制
    go channel 概述
    vue2.x入门学习
    vue-cli安装记录
    maven常用Java配置
    activiti工作流引擎
  • 原文地址:https://www.cnblogs.com/liushijie/p/9797646.html
Copyright © 2011-2022 走看看