前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Dubbo源码篇02---从泛化调用探究Wrapper机制的原理

Dubbo源码篇02---从泛化调用探究Wrapper机制的原理

作者头像
大忽悠爱学习
发布2023-05-09 16:26:01
7090
发布2023-05-09 16:26:01
举报
文章被收录于专栏:c++与qt学习

Dubbo源码篇02---从泛化调用探究Wrapper机制的原理


什么是泛化调用

从传统三层架构说起

对于传统的三层架构而言,Controller层负责接收请求,Service层负责处理与业务逻辑相关的请求,Dao层负责与数据库进行交互,配合Model模型对象承载业务数据,在请求上下文中传递,最终处理填充完毕数据后,交由View视图进行渲染:

但是在微服务场景下,Service层中难免会涉及到RPC远程调用请求,因此上面的流程图就变成了下面这样子:

上面是rpc远程调用的一种情况,还有一种情况如下所示:

在这种情况下,我们的Controller层代码可能如下所示:

代码语言:javascript
复制
@RestController
public class UserController {
    // 响应码为成功时的值
    public static final String SUCC = "000000";
    
    // 定义访问下游查询用户服务的字段
    @DubboReference
    private UserQueryFacade userQueryFacade;
    
    // 定义URL地址
    @PostMapping("/queryUserInfo")
    public String queryUserInfo(@RequestBody QueryUserInfoReq req){
        // 将入参的req转为下游方法的入参对象,并发起远程调用
        QueryUserInfoResp resp = 
                userQueryFacade.queryUserInfo(convertReq(req));
        
        // 判断响应对象的响应码,不是成功的话,则组装失败响应
        if(!SUCC.equals(resp.getRespCode())){
            return RespUtils.fail(resp);
        }
        
        // 如果响应码为成功的话,则组装成功响应
        return RespUtils.ok(resp);
    }
}

在这种情况下,我们的Web服务器只是编写代码把数据做了一下包装,然后给到下游系统,等收到下游系统返回的内容后,啥也不做直接返回给前端,此时Web服务器本质是在做一些透传性质的事情。

这里我们只写了一个接口,但是如果有很多接口的Controller层逻辑都是简单的透传,那么能不能把这个逻辑抽取成通用逻辑呢?


反射调用尝试优化

我们先尝试使用反射将透传逻辑的公共部分抽取出来:

  • 传入要调用的service服务接口,及要调用的服务接口名,然后通过反射获取对应的Method对象
  • 将请求参数序列化为JSON字符串,再通过反序列化,将JSON字符串转换为下游接口的入参对象
  • 通过method.invoke反射发起真正的远程调用,并拿到响应对象
  • 通过Ognl表达式语言从响应对象中取出respCode响应码做判断,并最终返回
代码语言:javascript
复制
@RestController
public class UserController {
    // 响应码为成功时的值
    public static final String SUCC = "000000";
    
    // 定义访问下游查询用户服务的字段
    @DubboReference
    private UserQueryFacade userQueryFacade;
    
    // 定义URL地址
    @PostMapping("/queryUserInfo")
    public String queryUserInfo(@RequestBody QueryUserInfoReq req){
        // 调用公共方法
        return commonInvoke(userQueryFacade, "queryUserInfo", req);
    }
    
    /**
     * 模拟公共的远程调用方法
     * @param reqObj:下游的接口的实例对象,即通过 @DubboReference 得到的对象。
     * @param mtdName:下游接口的方法名。
     * @param reqParams:需要请求到下游的数据。
     * @return 直接结果数据。
     */
    public static String commonInvoke(Object reqObj, String mtdName, Object reqParams) throws InvocationTargetException, IllegalAccessException {
        // 通过反射找到 reqObj(例:userQueryFacade) 中的 mtdName(例:queryUserInfo) 方法
        Method reqMethod = ReflectionUtils.findMethod(reqObj.getClass(), mtdName);
        // 并设置查找出来的方法可被访问
        ReflectionUtils.makeAccessible(reqMethod);
        
        // 通过序列化工具将 reqParams 序列化为字符串格式
        String reqParamsStr = JSON.toJSONString(reqParams);
        // 然后再将 reqParamsStr 反序列化为下游对象格式,并反射调用 invoke 方法
        Object resp =  reqMethod.invoke(reqObj, JSON.parseObject(reqParamsStr, reqMethod.getParameterTypes()[0]));
        
        // 判断响应对象的响应码,不是成功的话,则组装失败响应
        if(resp == null || !SUCC.equals(OgnlUtils.getValue(resp, "respCode"))){
            return RespUtils.fail(resp);
        }
        // 如果响应码为成功的话,则组装成功响应
        return RespUtils.ok(resp);
    }
}

  • OGNL(Object-Graph Navigation Language)是一种基于表达式语言的Java对象图导航工具,可以用于简化Java应用程序中的对象导航和操作。OGNL提供了一组简单而强大的语法和运算符,可以轻松地在Java对象图中导航,访问对象属性、调用方法等。
  • OGNL最初是为Struts框架开发的,用于在JSP页面中直接访问Java对象。但是,由于其灵活性和强大性,OGNL已经成为了许多Java框架和应用程序中常用的工具,如Spring框架、Hibernate ORM框架等。
  • 下面是一些常见的OGNL表达式示例:
    • 访问对象属性:user.name
    • 调用对象方法:user.getName()
    • 操作集合元素:users.{? #this.age >18}.name
    • 操作数组元素:items[0].name
    • 使用逻辑运算符:user.age > 18 && user.gender == “male”
    • 使用条件运算符:user.age > 18 ? “adult” : “minor”
  • OGNL的优点包括:
    • 语法简单:OGNL使用类似于Java的表达式语言,易于理解和使用。
    • 导航方便:OGNL可以轻松地在Java对象图中导航,访问对象属性、调用方法等。
    • 功能强大:OGNL提供了一组简单而强大的语法和运算符,可以完成许多复杂的对象操作。
    • 可扩展性:OGNL可以轻松地扩展和自定义,以满足不同的应用程序需求。
  • 总的来说,OGNL是一个功能强大、易于使用的Java对象导航工具,可以使Java应用程序的开发更加简单和高效。

虽然我们通过反射抽取了远程方法调用流程的公共逻辑,使得单个controller接口内部的逻辑精简了许多,但是我们仍然需要定义很多类似于queryUserInfo这样的请求接口,那么有没有办法以一个统一的请求接口作为入口地址,统一处理所有透传式接口请求呢?


泛化调用

要以一个统一的请求接口作为入口地址,其实就类似于DispatchServlet统一拦截处理所有servlet请求的思路一样,然后再由DispatcherServlet按照路由规则派发给各个控制器进行请求处理:

我们这里的思路,就是编写一个统一的次级控制处理器,拦截所有请求,按照上面封装好的通用逻辑,发起RPC请求调用,然后返回远程调用结果即可。

上面的代码改造思路如下:

  • 定义一个公共的次级控制处理器CommonController
  • 定义统一的URL路径/gateway/{className}/{mtdName}/request ,将className和mtdName做成请求路径的占位符
  • 修改请求业务参数格式定义,由对象转换String
  • 在原有的CommonInvoke逻辑中,利用类加载器加载ClassName对应的服务调用接口,然后想办法找到ClassName对应的实例对象
代码语言:javascript
复制
@RestController
public class CommonController {
    // 响应码为成功时的值
    public static final String SUCC = "000000";
    
    // 定义URL地址
    @PostMapping("/gateway/{className}/{mtdName}/request")
    public String commonRequest(@PathVariable String className,
                                @PathVariable String mtdName,
                                @RequestBody String reqBody){
        // 将入参的req转为下游方法的入参对象,并发起远程调用
        return commonInvoke(className, mtdName, reqBody);
    }
    
    /**
     * 模拟公共的远程调用方法
     * @param className:下游的接口归属方法的全类名。
     * @param mtdName:下游接口的方法名。
     * @param reqParamsStr:需要请求到下游的数据。
     * @return 直接返回下游的整个对象。
     */
    public static String commonInvoke(String className, 
                                      String mtdName, 
                                      String reqParamsStr) throws InvocationTargetException, IllegalAccessException, ClassNotFoundException {
        // 试图从类加载器中通过类名获取类信息对象
        Class<?> clz = CommonController.class.getClassLoader().loadClass(className);
        // 然后试图通过类信息对象想办法获取到该类对应的实例对象
        Object reqObj = tryFindBean(clz.getClass());
        
        // 通过反射找到 reqObj(例:userQueryFacade) 中的 mtdName(例:queryUserInfo) 方法
        Method reqMethod = ReflectionUtils.findMethod(clz, mtdName);
        // 并设置查找出来的方法可被访问
        ReflectionUtils.makeAccessible(reqMethod);
        
        // 将 reqParamsStr 反序列化为下游对象格式,并反射调用 invoke 方法
        Object resp =  reqMethod.invoke(reqObj, JSON.parseObject(reqParamsStr, reqMethod.getParameterTypes()[0]));
        
        // 判断响应对象的响应码,不是成功的话,则组装失败响应
        if(!SUCC.equals(OgnlUtils.getValue(resp, "respCode"))){
            return RespUtils.fail(resp);
        }
        // 如果响应码为成功的话,则组装成功响应
        return RespUtils.ok(resp);
    }
}

现在的关键问题就是tryFindBean方法中,我们该通过什么样的办法拿到服务调用接口的实例对象呢?或者说,该怎么仿照@DubboReference注解,拿到服务调用的实例对象呢?

  • 此时我们就需要使用到Dubbo提供的泛化调用特性了,即在调用方没有服务方提供的服务调用接口的情况下,对服务方进行调用,并且可以正常拿到调用结果。

泛化调用怎么用

环境准备:

  • 暴露的服务接口
代码语言:javascript
复制
public interface HelloService {
    String sayHello(String arg);
}
  • 提供服务接口的具体实现类,同时需要实现GenericService,表示为泛化调用
代码语言:javascript
复制
public class GenericImplOfHelloService implements GenericService,HelloService{
    @Override
    public Object $invoke(String method, String[] parameterTypes, Object[] args) throws GenericException {
        System.out.println("进行泛化调用");
        if(method.equals("sayHello")){
            return "泛化调用结果: "+sayHello(args[0].toString());
        }
        return null;
    }

    @Override
    public String sayHello(String arg) {
        return "hello "+arg;
    }
}

服务提供方使用API使用泛化调用的步骤:

  1. 在设置 ServiceConfig 时,使用setGeneric("true")来开启泛化调用
  2. 在设置 ServiceConfig 时,使用 setRef 指定实现类时,要设置一个 GenericService 的对象。而不是真正的服务实现类对象
  3. 其他设置与正常 Api 服务启动一致即可
  • 服务提供者完整代码
代码语言:javascript
复制
    @Test
    void genericProviderTest() throws InterruptedException {
        //创建ApplicationConfig
        ApplicationConfig applicationConfig = new ApplicationConfig();
        applicationConfig.setName("generic-impl-provider");
        //创建注册中心配置
        RegistryConfig registryConfig = new RegistryConfig();
        registryConfig.setAddress("zookeeper://8.134.144.48:2181");

        //新建服务实现类,注意要使用GenericService接收
        GenericService helloService = new GenericImplOfHelloService();

        //创建服务相关配置
        ServiceConfig<GenericService> service = new ServiceConfig<>();
        service.setApplication(applicationConfig);
        service.setRegistry(registryConfig);
        service.setInterface("dubbo.dubboSpi.HelloService");
        service.setRef(helloService);
        //重点:设置为泛化调用
        //注:不再推荐使用参数为布尔值的setGeneric函数
        //应该使用referenceConfig.setGeneric("true")代替
        service.setGeneric("true");
        service.export();

        System.out.println("dubbo service started");

        new CountDownLatch(1).await();
    }

服务消费方使用API使用泛化调用的步骤:

  1. 在设置 ReferenceConfig 时,使用 setGeneric("true") 来开启泛化调用
  2. 配置完 ReferenceConfig 后,使用 referenceConfig.get() 获取到 GenericService 类的实例
  3. 使用其 $invoke 方法获取结果
  4. 其他设置与正常 Api 服务启动一致即可
  • 服务消费者完整代码
代码语言:javascript
复制
    @Test
    void genericConsumerTest() throws InterruptedException {
        //创建ApplicationConfig
        ApplicationConfig applicationConfig = new ApplicationConfig();
        applicationConfig.setName("generic-call-consumer");
        //创建注册中心配置
        RegistryConfig registryConfig = new RegistryConfig();
        registryConfig.setAddress("zookeeper://8.134.144.48:2181");
        //创建服务引用配置
        ReferenceConfig<GenericService> referenceConfig = new ReferenceConfig<>();
        //设置接口
        referenceConfig.setInterface("dubbo.dubboSpi.HelloService");
        applicationConfig.setRegistry(registryConfig);
        referenceConfig.setApplication(applicationConfig);
        //重点:设置为泛化调用
        //注:不再推荐使用参数为布尔值的setGeneric函数
        //应该使用referenceConfig.setGeneric("true")代替
        referenceConfig.setGeneric(true);
        //设置异步,不必须,根据业务而定。
        referenceConfig.setAsync(true);
        //设置超时时间
        referenceConfig.setTimeout(7000);

        //获取服务,由于是泛化调用,所以获取的一定是GenericService类型
        GenericService genericService = referenceConfig.get();

        //使用GenericService类对象的$invoke方法可以代替原方法使用
        //第一个参数是需要调用的方法名
        //第二个参数是需要调用的方法的参数类型数组,为String数组,里面存入参数的全类名。
        //第三个参数是需要调用的方法的参数数组,为Object数组,里面存入需要的参数。
        Object result = genericService.$invoke("sayHello", new String[]{"java.lang.String"}, new Object[]{"world"});
        //使用CountDownLatch,如果使用同步调用则不需要这么做。
        CountDownLatch latch = new CountDownLatch(1);
        //获取结果
        CompletableFuture<String> future = RpcContext.getContext().getCompletableFuture();
        future.whenComplete((value, t) -> {
            System.err.println("invokeSayHello(whenComplete): " + value);
            latch.countDown();
        });
        //由于开启了异步模式,此处打印应该为null
        System.err.println("invokeSayHello(return): " + result);
        //打印结果
        latch.await();
    }
  • 测试

通过Spring使用泛化调用

Spring 中服务暴露与服务发现有多种使用方式,如 xml,注解。这里以注解为例。 步骤:

  1. 生产者端无需改动
  2. 消费者端原有的 @DubboReference 注解上指明interfaceClass和generic=true
代码语言:javascript
复制
public interface UserService {
    User getUserById(String id);
}

public class UserServiceImpl implements UserService {
    @Override
    public User getUserById(String id) {
        // Do something to get the user by id
        return user;
    }
}
代码语言:javascript
复制
@Service
public class UserServiceImpl implements UserService {
    @DubboReference(interfaceClass = UserService.class, generic = true)
    private GenericService genericService;

    @Override
    public User getUserById(String id) {
        Object result = genericService.$invoke("getUserById", new String[]{"java.lang.String"}, new Object[]{id});
        return (User) result;
    }
}

利用泛化调用改造现有服务

Dubbo消费端发起远程调用的核心类是ReferenceConfig,接下来要做的就是拿到 referenceConfig#get 返回的泛化对象 GenericService,然后调用 GenericService#$invoke 方法进行远程调用。

代码语言:javascript
复制
@RestController
public class CommonController {
    // 响应码为成功时的值
    public static final String SUCC = "000000";
    
    // 定义URL地址
    @PostMapping("/gateway/{className}/{mtdName}/{parameterTypeName}/request")
    public String commonRequest(@PathVariable String className,
                                @PathVariable String mtdName,
                                @PathVariable String parameterTypeName,
                                @RequestBody String reqBody){
        // 将入参的req转为下游方法的入参对象,并发起远程调用
        return commonInvoke(className, parameterTypeName, mtdName, reqBody);
    }
    
    /**
     * 模拟公共的远程调用方法
     * @param className:下游的接口归属方法的全类名。
     * @param mtdName:下游接口的方法名。
     * @param parameterTypeName:下游接口的方法入参的全类名。
     * @param reqParamsStr:需要请求到下游的数据。
     * @return 直接返回下游的整个对象。
     */
    public static String commonInvoke(String className,
                                      String mtdName,
                                      String parameterTypeName,
                                      String reqParamsStr) {
        // 然后试图通过类信息对象想办法获取到该类对应的实例对象
        ReferenceConfig<GenericService> referenceConfig = createReferenceConfig(className);
        
        // 远程调用
        GenericService genericService = referenceConfig.get();
        Object resp = genericService.$invoke(
                mtdName,
                new String[]{parameterTypeName},
                new Object[]{JSON.parseObject(reqParamsStr, Map.class)});
        
        // 判断响应对象的响应码,不是成功的话,则组装失败响应
        if(!SUCC.equals(OgnlUtils.getValue(resp, "respCode"))){
            return RespUtils.fail(resp);
        }
        
        // 如果响应码为成功的话,则组装成功响应
        return RespUtils.ok(resp);
    }
    
    private static ReferenceConfig<GenericService> createReferenceConfig(String className) {
        DubboBootstrap dubboBootstrap = DubboBootstrap.getInstance();
        
        // 设置应用服务名称
        ApplicationConfig applicationConfig = new ApplicationConfig();
        applicationConfig.setName(dubboBootstrap.getApplicationModel().getApplicationName());
        
        // 设置注册中心的地址
        String address = dubboBootstrap.getConfigManager().getRegistries().iterator().next().getAddress();
        RegistryConfig registryConfig = new RegistryConfig(address);
        ReferenceConfig<GenericService> referenceConfig = new ReferenceConfig<>();
        referenceConfig.setApplication(applicationConfig);
        referenceConfig.setRegistry(registryConfig);
        referenceConfig.setInterface(className);
        
        // 设置泛化调用形式
        referenceConfig.setGeneric("true");
        // 设置默认超时时间5秒
        referenceConfig.setTimeout(5 * 1000);
        return referenceConfig;
    }
}
  • URL 地址增加了一个方法参数类名的维度,意味着通过类名、方法名、方法参数类名可以访问后台的提供者;
  • 通过接口类名来创建 ReferenceConfig 对象,并设置 generic = true 的核心属性;
  • 通过 referenceConfig.get 方法得到 genericService 泛化对象;
  • 将方法名、方法参数类名、业务请求参数传入泛化对象的 $invoke 方法中进行远程 Dubbo 调用,并返回响应对象;
  • 通过 Ognl 表达式语言从响应对象取出 respCode 响应码判断并做最终返回

泛化调用小结

泛化调用是指在调用方没有服务方提供的 API(SDK)的情况下,对服务方进行调用,并且可以正常拿到调用结果。

泛化采用一种统一的方式来发起对任何服务方法的调用,至少我们知道是一种接口调用的方式,只是这种方式有一个比较独特的名字而已。

泛化调用有哪些应用场景呢?

泛化调用主要用于实现一个通用的远程服务 Mock 框架,可通过实现 GenericService 接口处理所有服务请求。比如如下场景:

  • 网关服务:如果要搭建一个网关服务,那么服务网关要作为所有 RPC 服务的调用端。但是网关本身不应该依赖于服务提供方的接口 API(这样会导致每有一个新的服务发布,就需要修改网关的代码以及重新部署),所以需要泛化调用的支持。
  • 测试平台:如果要搭建一个可以测试 RPC 调用的平台,用户输入分组名、接口、方法名等信息,就可以测试对应的 RPC 服务。那么由于同样的原因(即会导致每有一个新的服务发布,就需要修改网关的代码以及重新部署),所以平台本身不应该依赖于服务提供方的接口 API。所以需要泛化调用的支持。

详细可以阅读官方文档: 泛化调用(客户端泛化)


Wrapper机制

到此为止,我们已经利用了泛化调用将服务消费端改造为了一个通用的网关服务,但是服务提供方这边如何处理泛化请求呢?

  • 泛化请求会携带接口类名、接口方法名、接口方法参数类名、业务请求参数,这四个维度的字段发起远程调用。
  • 服务提供方服务,需要在统一的入口中接收请求,然后派发到不同的接口服务中去。

如果要针对这个统一的入口进行编码实现,你会怎么写呢?

最容易想到的思路便是通过反射机制获取接口类名对应的类对象,然后利用类对象从IOC容器中拿到对应的bean,通过接口方法名和接口方法参数,来精准定位需要提供方接口服务中的哪个方法进行处理。

代码语言:javascript
复制
@RestController
public class CommonController {
    // 定义统一的URL地址
    @PostMapping("gateway/{className}/{mtdName}/{parameterTypeName}/request")
    public String recvCommonRequest(@PathVariable String className,
                                    @PathVariable String mtdName,
                                    @PathVariable String parameterTypeName,
                                    @RequestBody String reqBody) throws Exception {
        // 统一的接收请求的入口
        return commonInvoke(className, parameterTypeName, mtdName, reqBody);
    }

    /**
     * 统一入口的核心逻辑
     *
     * @param className:接口归属方法的全类名。
     * @param mtdName:接口的方法名。
     * @param parameterTypeName:接口的方法入参的全类名。
     * @param reqParamsStr:请求数据。
     * @return 接口方法调用的返回信息。
     */
    public static String commonInvoke(String className,
                                      String mtdName,
                                      String parameterTypeName,
                                      String reqParamsStr) throws Exception {
        // 通过反射机制可以获取接口类名对应的类对象
        Class<?> clz = Class.forName(className);
        // 接着通过类对象从IOC容器中定位对应的bean实例
        Object cacheObj = SpringCtxUtils.getBean(clz);
        // 通过反射找到方法对应的 Method 对象,并调用执行
        Class<?> methodParamType = Class.forName(parameterTypeName);
        Method method = clz.getDeclaredMethod(mtdName,methodParamType);
        method.setAccessible(true);
        return (String) method.invoke(cacheObj, JSON.parseObject(reqParamsStr,methodParamType));
    }
}

通过反射调用实现起来很简单,但是问题在于反射调用比较耗时,dubbo作为一款追求高性能的调用框架,并没有采用反射来实现,性能最高的实现应该是直接调用目标对象的方法,如下所示:

代码语言:javascript
复制
// 来精准定位需要提供方接口服务中的哪个方法进行处理
if ("sayHello".equals(mtdName) && String.class.getName().equals(parameterTypeName)) {
    // 真正的发起对源对象(被代理对象)的方法调用
    return ((DemoFacade) cacheObj).sayHello(reqParamsStr);
} else if("say".equals(mtdName) && Void.class.getName().equals(parameterTypeName)){
    // 真正的发起对源对象(被代理对象)的方法调用
    return ((DemoFacade) cacheObj).say();
}

很显然,我们无法直接将这段逻辑移到commonInvoke方法内部,因为我们不可能在commonInvoke方法内部使用if…else硬编码出所有情况,这实在是不合理 !

解决办法是将上面的逻辑移动到对应的服务提供实现类中,即每个服务实现类继承Dubbo提供的GenericService接口:

代码语言:javascript
复制
public interface GenericService {
    Object $invoke(String method, String[] parameterTypes, Object[] args) throws GenericException;
    ...
}

在接口的$invoke方法中硬编码列举所有可能的调用情况,如下所示:

代码语言:javascript
复制
public class GenericImplOfHelloService implements GenericService,HelloService{
    @Override
    public Object $invoke(String method, String[] parameterTypes, Object[] args) throws GenericException {
        if(method.equals("sayHello")){
            return "泛化调用结果: "+sayHello(args[0].toString());
        }
        return null;
    }

    @Override
    public String sayHello(String arg) {
        return "hello "+arg;
    }
}

利用Dubbo提供的泛化接口改造上面的代码,结果如下:

代码语言:javascript
复制
@RestController
public class CommonController {
    // 定义统一的URL地址
    @PostMapping("gateway/{className}/{mtdName}/{parameterTypeName}/request")
    public Object recvCommonRequest(@PathVariable String className,
                                    @PathVariable String mtdName,
                                    @PathVariable String parameterTypeName,
                                    @RequestBody String reqBody) throws Exception {
        // 统一的接收请求的入口
        return commonInvoke(className, parameterTypeName, mtdName, reqBody);
    }

    /**
     * 统一入口的核心逻辑
     *
     * @param className:接口归属方法的全类名。
     * @param mtdName:接口的方法名。
     * @param parameterTypeName:接口的方法入参的全类名。
     * @param reqParamsStr:请求数据。
     * @return 接口方法调用的返回信息。
     */
    public static Object commonInvoke(String className,
                                      String mtdName,
                                      String parameterTypeName,
                                      String reqParamsStr) throws Exception {
        // 通过反射机制可以获取接口类名对应的类对象
        Class<?> clz = Class.forName(className);
        // 接着通过类对象的简称获取到对应的接口服务
        GenericService genericService = SpringCtxUtils.getBean(clz);
        // 调用泛化接口的invoke方法
        return genericService.$invoke(mtdName,new String[]{parameterTypeName},new Object[]{JSON.parseObject(reqParamsStr,Class.forName(parameterTypeName))});
    }
}

目前看上去一切都很完美,唯一不完美的地方在于服务提供方的每个服务实现类都需要实现GenericService接口,并重写invoke方法,并在方法内部硬编码好相关的调用逻辑。

其实我们可以利用动态代理来将上面硬编码的重复逻辑抽取出来,动态代理常用的有JDK动态代理和Cglib动态代理,这里首先排除JDK动态代理,因为JDK动态代理采用的也是反射调用。

而Cglib 的核心原理,是通过执行拦截器的回调方法(methodProxy.invokeSuper),从代理类的众多方法引用中匹配正确方法,并执行被代理类的方法。

Cglib 的这种方式,就像代理类的内部动态生成了一堆的 if…else 语句来调用被代理类的方法,避免了手工写各种 if…else 的硬编码逻辑,省去了不少硬编码的活。

但是Dubbo没有采用cglib实现动态代理,因为Cglib的核心实现是生成了各种 if…else 代码来调用被代理类的方法,但是这块生成代理的逻辑不够灵活,难以自主修改。

Dubbo在 Cglib 的思想之上采用自主实现,并且不使用反射机制, 打造一个简化版的迷你型 Cglib 代理工具,这样一来,就可以在自己的代理工具中做各种与框架密切相关的逻辑了。


自定义代理

既然要自己生成代理类,就得先按照一个代码模板来编码,我们来设计代码模板:

代码语言:javascript
复制
public interface HelloService {
    String sayHello(String arg,String name);
    String test();
}


public class HelloServiceImpl implements HelloService{
    @Override
    public String sayHello(String arg, String name) {
        return "hello "+arg+" "+name;
    }

    @Override
    public String test() {
        return "test";
    }
}


//代理类模板
public class $GenericImplOfHelloService_1 extends HelloServiceImpl implements GenericService {
    public java.lang.Object $invoke(String method, String[] parameterTypes, Object[] args) throws GenericException {
        if ("test".equals(method) && (parameterTypes == null || parameterTypes != null && parameterTypes.length == 0)) {
            return test();
        }
        if ("sayHello".equals(method) && (parameterTypes != null && parameterTypes.length == 2 && parameterTypes[0].equals("java.lang.String") && parameterTypes[1].equals("java.lang.String"))) {
            return sayHello((java.lang.String) args[0], (java.lang.String) args[1]);
        }
        throw new GenericException(new NoSuchMethodException("Method [" + method + "] not found."));
    }
} 

有了代码模板,我们对照着代码模板用 Java 语言编写生成出来:

代码语言:javascript
复制
package com.provider.wrapperDemo;
import org.apache.dubbo.rpc.service.GenericException;
import org.apache.dubbo.rpc.service.GenericService;


import javax.tools.JavaCompiler;
import javax.tools.JavaFileObject;
import javax.tools.StandardJavaFileManager;
import javax.tools.ToolProvider;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicInteger;

public class CustomInvokerProxyUtils {
    private static final String WRAPPER_PACKAGE_NAME = "dubbo.dubboSpi";
    private static final AtomicInteger INC = new AtomicInteger();


    /**
     * 创建源对象(被代理对象)的代理对象
     */
    public static GenericService newProxyInstance(Object sourceTarget) throws Exception {
        // 代理类文件保存的磁盘路径
        String filePath = getWrapperBasePath();
        // 获取服务接口类
        Class<?> targetClazz = sourceTarget.getClass().getInterfaces()[0];
        // 生成的代理类名称:  $GenericImplOfHelloService_1
        String proxyClassName = "$GenericImplOf" + targetClazz.getSimpleName() + "_" + INC.incrementAndGet();
        // 获取代理的字节码内容
        String proxyByteCode = getProxyByteCode(proxyClassName, targetClazz, sourceTarget.getClass());
        // 缓存至磁盘中
        file2Disk(filePath, proxyClassName, proxyByteCode);
        // 等刷盘稳定后
        Thread.sleep(2000);
        // 再编译java加载class至内存中,返回实例化的对象
        return (GenericService) compileJava2Class(filePath, proxyClassName);
    }

    private static String getWrapperBasePath() {
        return Objects.requireNonNull(CustomInvokerProxyUtils.class.getResource("/")).getPath()
                + CustomInvokerProxyUtils.class.getPackage().toString().substring("package ".length()).replaceAll("\\.", "/");
    }

    /**
     * 生成代理的字节码内容,其实就是一段类代码的字符串
     */
    private static String getProxyByteCode(String proxyClassName, Class<?> targetClazz, Class<?> sourceClass) {
        StringBuilder sb = new StringBuilder();
        // java文件第一行是package声明包路径
        String pkgContent = "package " + CustomInvokerProxyUtils.WRAPPER_PACKAGE_NAME + ";";
        //通过import导入代理类中可能会使用到的类
        String importTargetClazzImpl = "import " + sourceClass.getName() + ";";
        String importGenericService = "import " + GenericService.class.getName() + ";";
        String importGenericException = "import " + GenericException.class.getName() + ";";
        String importNoSuchMethodException="import "+ NoSuchMethodException.class.getName()+";";
        // 代理类主体内容构建
        String classHeadContent = "public class " + proxyClassName + " extends " + sourceClass.getSimpleName() + " implements " + GenericService.class.getSimpleName() + " {";
        // 添加内容
        sb.append(pkgContent).append(importTargetClazzImpl).append(importGenericService).append(importGenericException)
                .append(importNoSuchMethodException).append(classHeadContent);
        // 构建invoke方法
        String invokeMethodHeadContent = "public " + Object.class.getName() + " $invoke" +
                "(" + String.class.getSimpleName() + " method, "
                + String.class.getSimpleName() + "[] parameterTypes, "
                + Object.class.getSimpleName() + "[] args) throws " + GenericException.class.getSimpleName() + " {\n";
        sb.append(invokeMethodHeadContent);
        // 组装if...else...逻辑
        for (Method method : targetClazz.getMethods()) {
            String methodName = method.getName();
            Class<?>[] parameterTypes = method.getParameterTypes();
            String ifHead = "if (\"" + methodName + "\".equals(method)"+buildMethodParamEqual(parameterTypes)+") {\n";
            //组装方法调用逻辑
            String ifContent = buildMethodInvokeContent(methodName, parameterTypes);
            String ifTail = "}\n";
            sb.append(ifHead).append(ifContent).append(ifTail);
        }
        // throw new GenericException("Method [" + method + "] not found.");
        String invokeMethodTailContent = "throw new " + GenericException.class.getSimpleName() + "(new NoSuchMethodException(\"Method [\" + method + \"] not found.\"));\n}\n";
        sb.append(invokeMethodTailContent);
        // 类的尾巴大括号
        String classTailContent = " } ";
        sb.append(classTailContent);
        return sb.toString();
    }

    private static String buildMethodParamEqual(Class<?>[] parameterTypes) {
        StringBuilder methodParamEqualContent=new StringBuilder();
        methodParamEqualContent.append("&&(");
        //方法参数为0,则可以传入null
        if(parameterTypes.length==0){
            methodParamEqualContent.append("parameterTypes==null ||");
        }
        //参数类型必须合法
        methodParamEqualContent.append(" parameterTypes!=null&&parameterTypes.length==").append(parameterTypes.length);
        for (int i = 0; i < parameterTypes.length; i++) {
            methodParamEqualContent.append("&&parameterTypes[").append(i).append("].equals(\"").append(parameterTypes[i].getName()).append("\")");
        }
        methodParamEqualContent.append(")");
        return methodParamEqualContent.toString();
    }

    private static String buildMethodInvokeContent(String methodName, Class<?>[] parameterTypes) {
        if (parameterTypes.length == 0) {
            return "return " + methodName + "();\n";
        }
        StringBuilder methodInvokeContent = new StringBuilder();
        methodInvokeContent.append("return ").append(methodName).append("(");
        for (int i = 0; i < parameterTypes.length; i++) {
            methodInvokeContent.append("(").append(parameterTypes[i].getName()).append(")")
                    .append("args[").append(i).append("]").append(",");
        }
        //删除最后一个多余的,
        methodInvokeContent.delete(methodInvokeContent.length()-1,methodInvokeContent.length());
        methodInvokeContent.append(");\n");
        return methodInvokeContent.toString();
    }

    private static void file2Disk(String filePath, String proxyClassName, String proxyByteCode) throws IOException {
        File file = new File(filePath + File.separator + proxyClassName + ".java");
        if (!file.exists()) {
            file.createNewFile();
        }
        FileWriter fileWriter = new FileWriter(file);
        fileWriter.write(proxyByteCode);
        fileWriter.flush();
        fileWriter.close();
    }

    private static Object compileJava2Class(String filePath, String proxyClassName) throws Exception {
        // 编译 Java 文件
        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
        StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null);
        Iterable<? extends JavaFileObject> compilationUnits =
                fileManager.getJavaFileObjects(new File(filePath + File.separator + proxyClassName + ".java"));
        JavaCompiler.CompilationTask task = compiler.getTask(null, fileManager, null, null, null, compilationUnits);
        task.call();
        fileManager.close();
        // 加载 class 文件
        URL[] urls = new URL[]{new URL("file:" + filePath)};
        URLClassLoader urlClassLoader = new URLClassLoader(urls);

        Class<?> clazz = urlClassLoader.loadClass(CustomInvokerProxyUtils.WRAPPER_PACKAGE_NAME + "." + proxyClassName);
        // 反射创建对象,并且实例化对象
        Constructor<?> constructor = clazz.getConstructor();
        return constructor.newInstance();
    }
}

生成的代码主要有三个步骤:

  • 按照代码模板的样子,使用 Java 代码动态生成出来一份代码字符串。
  • 将生成的代码字符串保存到磁盘中。
  • 根据磁盘文件路径将文件编译为 class 文件,然后利用 URLClassLoader 加载至内存变成 Class 对象,最后反射创建对象并且实例化对象。

注意: 如果抛出下面这个异常

代码语言:javascript
复制
 Caused by: java.lang.ClassNotFoundException: com.sun.tools.javac.processing.JavacProcessingEnvironment

可能是因为缺少 tools.jar 这个依赖,可以在maven中引入相关依赖:

代码语言:javascript
复制
<dependency>
  <groupId>com.sun</groupId>
  <artifactId>tools</artifactId>
  <version>1.8.0</version>
  <scope>system</scope>
  <systemPath>${java.home}/../lib/tools.jar</systemPath>
</dependency>

请注意,system 作用域将使 Maven 不会从远程仓库中下载这个依赖,而是使用指定的 systemPath。你需要根据你的环境修改 systemPath 的值,确保它指向 tools.jar 的实际路径。

测试:

代码语言:javascript
复制
    @Test
    void wrapperDemoTest() throws Exception {
        GenericService genericService = CustomInvokerProxyUtils.newProxyInstance(new HelloServiceImpl());
        Object testMethodRes = genericService.$invoke("test", null, null);
        System.out.println("test method invoke res: "+testMethodRes);
        Object sayHelloRes = genericService.$invoke("sayHello", new String[]{String.class.getName(),String.class.getName()}, new Object[]{"参数", "大忽悠"});
        System.out.println("sayHello method invoke res: "+sayHelloRes);
    }

生成的java文件:


dubbo底层wrapper原理

上面通过代理类实现GenericService泛化接口,是我根据Dubbo官方文档泛化调用(客户端泛化)一节,服务提供者端代码启发而作,dubbo官方实现思路类似,但是代理类并非实现GenericService接口,下面我们一起来看看。

dubbo创建的代理类继承了Wrapper接口,实现类比上面给出的GenericService:

代码语言:javascript
复制
// org.apache.dubbo.rpc.proxy.javassist.JavassistProxyFactory#getInvoker
// 创建一个 Invoker 的包装类
@Override
public <T> Invoker<T> getInvoker(T proxy, Class<T> type, URL url) {
    // 这里就是生成 Wrapper 代理对象的核心一行代码
    final Wrapper wrapper = Wrapper.getWrapper(proxy.getClass().getName().indexOf('$') < 0 ? proxy.getClass() : type);
    // 包装一个 Invoker 对象
    return new AbstractProxyInvoker<T>(proxy, type, url) {
        @Override
        protected Object doInvoke(T proxy, String methodName,
                                  Class<?>[] parameterTypes,
                                  Object[] arguments) throws Throwable {
            // 使用 wrapper 代理对象调用自己的 invokeMethod 方法
            // 以此来避免反射调用引起的性能开销
            // 通过强转来实现统一方法调用
            return wrapper.invokeMethod(proxy, methodName, parameterTypes, arguments);
        }
    };
}

使用Dubbo提供的Wrapper创建代理对象,并进行方法调用演示:

代码语言:javascript
复制
    @Test
    void dubboWrapperTest() throws InvocationTargetException {
        HelloService helloService = new HelloServiceImpl();
        final Wrapper wrapper = Wrapper.getWrapper(helloService.getClass());
        Object testMethodRes = wrapper.invokeMethod(helloService, "test", new Class[]{},null);
        System.out.println("test method invoke res: " + testMethodRes);
        Object sayHelloRes = wrapper.invokeMethod(helloService,"sayHello",new Class[]{String.class, String.class}, new Object[]{"参数", "大忽悠"});
        System.out.println("sayHello method invoke res: " + sayHelloRes);
    }

我们把生成的 wrapper 代理类 class 文件反编译为 Java 代码,看看生成的内容到底长什么样的,这里需要借助提供的Arthas工具完成:

  1. 启动测试用例,并在测试方法结尾调用System.in.read()方法挂起当前线程
  2. 启动Arthas: java -jar arthas-boot.jar
  1. 模糊搜索所有dubbo生成的代理类

5. 查看对应代理类的完整代码

代码语言:javascript
复制
package dubbo.dubboSpi;

import dubbo.dubboSpi.HelloServiceImpl;
import java.lang.reflect.InvocationTargetException;
import java.util.Map;
import org.apache.dubbo.common.bytecode.ClassGenerator;
import org.apache.dubbo.common.bytecode.NoSuchMethodException;
import org.apache.dubbo.common.bytecode.NoSuchPropertyException;
import org.apache.dubbo.common.bytecode.Wrapper;

public class HelloServiceImplDubboWrap0 extends Wrapper implements ClassGenerator.DC {
    public static String[] pns;
    public static Map pts;
    public static String[] mns;
    public static String[] dmns;
    public static Class[] mts0;
    public static Class[] mts1;

    @Override
    public String[] getPropertyNames() {
        return pns;
    }

    @Override
    public boolean hasProperty(String string) {
        return pts.containsKey(string);
    }

    public Class getPropertyType(String string) {
        return (Class)pts.get(string);
    }

    @Override
    public String[] getMethodNames() {
        return mns;
    }

    @Override
    public String[] getDeclaredMethodNames() {
        return dmns;
    }

    @Override
    public void setPropertyValue(Object object, String string, Object object2) {
        try {
            HelloServiceImpl helloServiceImpl = (HelloServiceImpl)object;
        }
        catch (Throwable throwable) {
            throw new IllegalArgumentException(throwable);
        }
        throw new NoSuchPropertyException(new StringBuffer().append("Not found property \"").append(string).append("\" field or setter method in class dubbo.dubboSpi.HelloServiceImpl.").toString());
    }

    @Override
    public Object getPropertyValue(Object object, String string) {
        try {
            HelloServiceImpl helloServiceImpl = (HelloServiceImpl)object;
        }
        catch (Throwable throwable) {
            throw new IllegalArgumentException(throwable);
        }
        throw new NoSuchPropertyException(new StringBuffer().append("Not found property \"").append(string).append("\" field or getter method in class dubbo.dubboSpi.HelloServiceImpl.").toString());
    }

    public Object invokeMethod(Object object, String string, Class[] classArray, Object[] objectArray) throws InvocationTargetException {
        HelloServiceImpl helloServiceImpl;
        try {
            helloServiceImpl = (HelloServiceImpl)object;
        }
        catch (Throwable throwable) {
            throw new IllegalArgumentException(throwable);
        }
        try {
            if ("sayHello".equals(string) && classArray.length == 2) {
                return helloServiceImpl.sayHello((String)objectArray[0], (String)objectArray[1]);
            }
            if ("test".equals(string) && classArray.length == 0) {
                return helloServiceImpl.test();
            }
        }
        catch (Throwable throwable) {
            throw new InvocationTargetException(throwable);
        }
        throw new NoSuchMethodException(new StringBuffer().append("Not found method \"").append(string).append("\" in class dubbo.dubboSpi.HelloServiceImpl.").toString());
    }
}

上面dubbo生成的动态代理类中,我们暂时只需要重点关注invokeMethod方法逻辑即可,可以看出其实和我们上面所讲的逻辑是一致的。

wrapper类源码本文不做展开,后续章节再深入分析。


小结

我们从服务提供方设计统一入口为题进行切入,从反射调用改造,到尝试硬编码提到性能,从而引出了自定义动态代理,虽然Cglib代理实现逻辑符合改造诉求,但是对于定制生成代理类的灵活需求,还得受Cglib库的限制。

因此,考虑上诉因素后,dubbo自定义了一个迷你型的Cglib代理工具,总体实现思路为:

  1. 先设计出一套通用的代码模板,使其具备业务场景的通用性,方便进行统一代理
  2. 通过手写java代码或者通过字节码工具,按照代码模板要求生成一套动态的代码
  3. 最后,将动态生成的代码通过JDK编译或者通过字节码工具,最终想办法生成Class对象
  4. 然后拿着生成的Class对象创建一个实例,用实例对象进行方法调用

小结

本文参考Dubbo官网提供的源码解析部分,综合个人理解,如有错误,欢迎评论区指出。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2023-05-06,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Dubbo源码篇02---从泛化调用探究Wrapper机制的原理
  • 什么是泛化调用
    • 从传统三层架构说起
      • 反射调用尝试优化
    • 泛化调用
      • 泛化调用怎么用
      • 利用泛化调用改造现有服务
    • 泛化调用小结
    • Wrapper机制
      • 自定义代理
        • dubbo底层wrapper原理
          • 小结
          • 小结
          相关产品与服务
          微服务引擎 TSE
          微服务引擎(Tencent Cloud Service Engine)提供开箱即用的云上全场景微服务解决方案。支持开源增强的云原生注册配置中心(Zookeeper、Nacos 和 Apollo),北极星网格(腾讯自研并开源的 PolarisMesh)、云原生 API 网关(Kong)以及微服务应用托管的弹性微服务平台。微服务引擎完全兼容开源版本的使用方式,在功能、可用性和可运维性等多个方面进行增强。
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档