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


什么是泛化调用

从传统三层架构说起

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

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

上面是rpc远程调用的一种情况,还有一种情况如下所示:
在这里插入图片描述
在这种情况下,我们的Controller层代码可能如下所示:

@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响应码做判断,并最终返回
@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对应的实例对象
@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提供的泛化调用特性了,即在调用方没有服务方提供的服务调用接口的情况下,对服务方进行调用,并且可以正常拿到调用结果。

泛化调用怎么用

环境准备:

  • 暴露的服务接口
public interface HelloService {
    String sayHello(String arg);
}
  • 提供服务接口的具体实现类,同时需要实现GenericService,表示为泛化调用
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 服务启动一致即可
  • 服务提供者完整代码
    @Test
    void genericProviderTest() throws InterruptedException {
        //创建ApplicationConfig
        ApplicationConfig applicationConfig = new ApplicationConfig();
        applicationConfig.setName("generic-impl-provider");
        //创建注册中心配置
        RegistryConfig registryConfig = new RegistryConfig();
        registryConfig.setAddress("zookeeper://127.0.0.1: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 服务启动一致即可
  • 服务消费者完整代码
    @Test
    void genericConsumerTest() throws InterruptedException {
        //创建ApplicationConfig
        ApplicationConfig applicationConfig = new ApplicationConfig();
        applicationConfig.setName("generic-call-consumer");
        //创建注册中心配置
        RegistryConfig registryConfig = new RegistryConfig();
        registryConfig.setAddress("zookeeper://127.0.0.1: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
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;
    }
}
@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 方法进行远程调用。

@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。所以需要泛化调用的支持。

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


服务方如何处理泛化调用请求

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

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

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

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

@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作为一款追求高性能的调用框架,并没有采用反射来实现,性能最高的实现应该是直接调用目标对象的方法,如下所示:

// 来精准定位需要提供方接口服务中的哪个方法进行处理
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接口:

public interface GenericService {
    Object $invoke(String method, String[] parameterTypes, Object[] args) throws GenericException;
    ...
}

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

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提供的泛化接口改造上面的代码,结果如下:

@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 代理工具,这样一来,就可以在自己的代理工具中做各种与框架密切相关的逻辑了。


自定义代理

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

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 语言编写生成出来:

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 对象,最后反射创建对象并且实例化对象。

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

 Caused by: java.lang.ClassNotFoundException: com.sun.tools.javac.processing.JavacProcessingEnvironment

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

<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 的实际路径。

测试:

    @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文件:
在这里插入图片描述


泛化调用底层原理

消费端

泛化调用的从消费端的referenceConfig.get()方法开始:

// get方法返回的是ReferenceConfig类内部的ref属性值,因此我们的思路就是追踪ref的赋值时机
GenericService genericService = referenceConfig.get();

ref属性在get—>init–>createProxy方法中被赋值:

ref = createProxy(referenceParameters);

createProxy方法,见名之意,可以猜到是为我们的服务接口创建一个代理对象,然后返回:

服务接口的实现类在服务提供方的包中,因此想要消费端可以直接拿着服务接口进行调用,就需要为服务接口创建一个代理对象,帮助用户屏蔽底层RPC调用细节。

    private T createProxy(Map<String, String> referenceParameters) {
        ...
       //获取所有可用注册中心的地址(Url),放入消费者配置类(AbstractInterfaceConfig)的urls集合中保存
       //(这里省略点点直连和本地两个jvm进程通信的jvm协议)
       aggregateUrlFromRegistry(referenceParameters);
       //创建负责进行远程调用的Invoker对象,该对象是dubbo调用的核心
       createInvokerForRemote();
       ...
        URL consumerUrl = new ServiceConfigURL(CONSUMER_PROTOCOL, referenceParameters.get(REGISTER_IP_KEY), 0,
        referenceParameters.get(INTERFACE_KEY), referenceParameters);
        consumerUrl = consumerUrl.setScopeModel(getScopeModel());
        consumerUrl = consumerUrl.setServiceModel(consumerModel);
        ...
       //为服务接口创建代理对象。=
       return (T) proxyFactory.getProxy(invoker, ProtocolUtils.isGeneric(generic));
    }

referenceParameters中存储客户端配置参数:
在这里插入图片描述
consumerUrl存放客户端请求相关配置信息:
在这里插入图片描述


创建Invoker对象

createInvokerForRemote根据拿到的注册中心url,

    //这里省略点点直连的场景 
    private void createInvokerForRemote() {
       //此处我们的urls集合中的url只有一个,有多个逻辑这里跳过不看
       //如果urls的长度为1,说明只有一个服务提供者,则直接通过protocolSPI.refer方法创建一个Invoker实例,
       //如果这个服务提供者不是注册中心,则使用StaticDirectory对这个Invoker进行包装。
       //StaticDirectory是Dubbo框架中的一个类,用于将一组Invoker封装成一个目录,以便消费者调用
        if (urls.size() == 1) {
            //拿到我们注册中心的URL
            URL curUrl = urls.get(0);
            // 通过SPI机制,调用protocolSPI扩展口子实现类的refer方法
            //为当前url创建并返回一个invoker请求调用对象实例
            invoker = protocolSPI.refer(interfaceClass, curUrl);
            ...
        }
        ...
    }

protocolSPI这里使用到了dubbo的Adaptive动态适配机制,这里会为ProtocolSPI类创建一个代理对象,代理对象的refer方法如下所示:

public class Protocol$Adaptive implements Protocol {
    public Invoker refer(Class clazz, URL uRL) throws RpcException {
        String string;
        if (uRL == null) {
            throw new IllegalArgumentException("url == null");
        }
        //url内部的protocol属性默认为dubbo
        string = uRL.getProtocol() == null ? "dubbo" : uRL.getProtocol();
        if (string == null) {
            throw new IllegalStateException("Failed to get extension (org.apache.dubbo.rpc.Protocol) name from url (" + uRL + ") use keys([protocol])");
        }
        ScopeModel scopeModel = ScopeModelUtil.getOrDefault(uRL.getScopeModel(), Protocol.class);
        //因此这里在url的协议类型为dubbo的情况下,默认拿到的扩展口子实现类类型为DubboProtocol
        Protocol protocol = scopeModel.getExtensionLoader(Protocol.class).getExtension(string);
        return protocol.refer(clazz, uRL);
    }
    ...

DubboProtocol类的refer方法

    @Override
    public <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException {
        ... 
        return protocolBindingRefer(type, url);
    }

    public <T> Invoker<T> protocolBindingRefer(Class<T> serviceType, URL url) throws RpcException {
        ... 
        // create rpc invoker.
        // dubbo总共分为十层,各个层之间的交互主要是通过Inovker完成的,可以理解分层的实现是Invoker套Invoker
        //这里只需要知道invoker的doInvoke方法中会完成本层应该做的逻辑
        DubboInvoker<T> invoker = new DubboInvoker<T>(serviceType, url, getClients(url), invokers);
        invokers.add(invoker);
        ...
        return invoker;
    }    

为服务接口创建代理对象

dubbo会为创建出来的invoker对象外面披上一层皮,这层皮就是代理对象:

  // invoker调用,当前调用是否为泛化调用
  return (T) proxyFactory.getProxy(invoker, ProtocolUtils.isGeneric(generic));

proxyFactory这里同样使用到了dubbo的Adaptive动态适配机制,这里会为ProxyFactory类创建一个代理对象,代理对象的getProxy方法如下所示:

public class ProxyFactory$Adaptive implements ProxyFactory {
    public Object getProxy(Invoker invoker, boolean bl) throws RpcException {
        ...
        //获取url
        URL uRL = invoker.getUrl();
        //获取url中的proxy属性,决定采用何种代理方式
        //如果我们没有设置,那么默认值为javassist
        String string = uRL.getParameter("proxy", "javassist");
        ...
        ScopeModel scopeModel = ScopeModelUtil.getOrDefault(uRL.getScopeModel(), ProxyFactory.class);
        //根据url中提供的代理key,利用dubbo的SPI机制去加载代理扩展口子提供的实现类
        ProxyFactory proxyFactory = scopeModel.getExtensionLoader(ProxyFactory.class).getExtension(string);
        //调用实现类的getProxy方法创建代理对象并返回
        return proxyFactory.getProxy(invoker, bl);
    }
    ...
}

AbstractProxyFactory存放代理工厂的公共配置,双参的getProxy方法就位于该类中, 抽象父类主要负责将代理类需要实现接口的公共逻辑抽取出来:

public abstract class AbstractProxyFactory implements ProxyFactory {
    private static final Class<?>[] INTERNAL_INTERFACES = new Class<?>[]{
        EchoService.class, Destroyable.class
    };
    
    @Override
    public <T> T getProxy(Invoker<T> invoker, boolean generic) throws RpcException {
        // interfaces用于存放代理类需要存放的所有接口集合
        LinkedHashSet<Class<?>> interfaces = new LinkedHashSet<>();
        //获取用于加载代理类需要继承接口的类加载器
        ClassLoader classLoader = getClassLoader(invoker);
        ...
        Class<?> realInterfaceClass = null;
        //如果当前客户端请求是泛化调用请求
        if (generic) {
            try {
                // 从consumerUrl中获取真实调用的服务提供者接口---这里为com.provider.one.DynamicDebugService
                String realInterface = invoker.getUrl().getParameter(Constants.INTERFACE);
                //尝试看能否实例化接口成功,如果成功,说明客户端这边存在服务提供者接口
                //如果采用泛化调用,一般都是不存在服务提供者接口的---所以这里会实例化失败
                realInterfaceClass = ReflectUtils.forName(classLoader, realInterface);
                interfaces.add(realInterfaceClass);
            } catch (Throwable e) {
                // ignore
            }
            //泛化调用的情况,invoker的getInterface返回的是GenericService.class
            //invoker的interface泛化调用情况下的赋值时机在ReferenceConfig的checkAndUpdateSubConfigs方法中
            if (GenericService.class.equals(invoker.getInterface()) || !GenericService.class.isAssignableFrom(invoker.getInterface())) {
                interfaces.add(com.alibaba.dubbo.rpc.service.GenericService.class);
            }
        }

        interfaces.add(invoker.getInterface());
        //创建的每个代理类额外会实现两个接口: EchoService.class, Destroyable.class
        // spring中实现的代理类额外也会使用几个接口,比如: Advised
        interfaces.addAll(Arrays.asList(INTERNAL_INTERFACES));

        try {
            //按照不同的技术实现创建代理对象,这里就交给子类实现了
            return getProxy(invoker, interfaces.toArray(new Class<?>[0]));
        } ...
    }
    ...
}

这里我们先不看dubbo提供的较为复杂的JavassistProxyFactory实现,先看最简单的JdkProxyFactory实现:

public class JdkProxyFactory extends AbstractProxyFactory {

    @Override
    @SuppressWarnings("unchecked")
    public <T> T getProxy(Invoker<T> invoker, Class<?>[] interfaces) {
        return (T) Proxy.newProxyInstance(invoker.getInterface().getClassLoader(), interfaces, new
 //代理类方法拦截的逻辑都在InvokerInvocationHandler类中实现
 InvokerInvocationHandler(invoker));
    }
    ...
}

后续流程就不继续展开了,只需要知道会将泛化请求经由Invoker调用链最终发送出去即可。


服务端

服务端接收到客户端的dubbo请求后,会经过每一层的处理,然后将请求交给invoker链进行处理,其中泛化请求会被GenericFilter的invoke方法所处理,我们来看看:

   @Override
    public Result invoke(Invoker<?> invoker, Invocation inv) throws RpcException {
        //这里校验此次请求是否为泛化调用---首先校验调用的是否是名为$Invoke方法
        //并且方法参数个数是否匹配
        if ((inv.getMethodName().equals($INVOKE) || inv.getMethodName().equals($INVOKE_ASYNC))
                && inv.getArguments() != null
                && inv.getArguments().length == 3
                && !GenericService.class.isAssignableFrom(invoker.getInterface())) {
            //获取泛化调用GenericService#$invoke方法需要的三个参数
            //方法名,方法参数类型数组,方法实参数组
            String name = ((String) inv.getArguments()[0]).trim();
            String[] types = (String[]) inv.getArguments()[1];
            Object[] args = (Object[]) inv.getArguments()[2];
            try {
                //从要调用的服务接口中,通过反射手段获取到要调用的目标方法对象
                Method method = ReflectUtils.findMethodByMethodSignature(invoker.getInterface(), name, types);
                Class<?>[] params = method.getParameterTypes();
                if (args == null) {
                    args = new Object[params.length];
                }
                if(types == null) {
                    types = new String[params.length];
                }
                //判断实参方法个数和目标方法的个数是否匹配,不匹配抛出异常
                if (args.length != types.length) {
                    throw new RpcException("GenericFilter#invoke args.length != types.length, please check your "
                            + "params");
                }
                //通过generic标志判断是否为泛化调用
                String generic = inv.getAttachment(GENERIC_KEY);     
                if (StringUtils.isBlank(generic)) {
                    generic = RpcContext.getClientAttachment().getAttachment(GENERIC_KEY);
                }
                //在是泛化调用的前提下
                if (StringUtils.isEmpty(generic)
                        || ProtocolUtils.isDefaultGenericSerialization(generic)
                        || ProtocolUtils.isGenericReturnRawResult(generic)) {
                    try {
                        //Dubbo提供的PojoUtils是一个用于处理Java对象的工具类,
                        //它可以将Java对象转换成Map对象或者将Map对象转换成Java对象。
                        //这里就是尝试将实参值转换为方法参数实际需要的类型
                        args = PojoUtils.realize(args, params, method.getGenericParameterTypes());
                    } catch (IllegalArgumentException e) {
                        throw new RpcException(e);
                    }
                }
                //其他类型转换情况的处理,这里不多展开
                ...
                
                //将泛化调用的请求调用上下文替换为目标方法调用的上下文    
                RpcInvocation rpcInvocation =
                        new RpcInvocation(invoker.getUrl().getServiceModel(), method, invoker.getInterface().getName(), invoker.getUrl().getProtocolServiceKey(), args,
                                inv.getObjectAttachments(), inv.getAttributes());
                rpcInvocation.setInvoker(inv.getInvoker());
                rpcInvocation.setTargetServiceUniqueName(inv.getTargetServiceUniqueName());
                //此时请求执行上下文已经被替换,所以后续dubbo会去调用目标服务实现类的目标方法
                return invoker.invoke(rpcInvocation);
            } catch (NoSuchMethodException | ClassNotFoundException e) {
                throw new RpcException(e.getMessage(), e);
            }
        }
        //非泛化调用情况
        return invoker.invoke(inv);
    }

原始的请求上下文:
在这里插入图片描述
替换后的请求上下文:
在这里插入图片描述
简单来说GenericFilter主要完成请求上下文的替换,由原始的泛化请求上下文替换为真实要调用的目标方法请求上下文,这个过程中主要涉及方法参数类型转换的问题。


wrapper机制原理

上面通过代理类实现GenericService泛化接口,是我根据Dubbo官方文档泛化调用(客户端泛化)一节,服务提供者端代码启发而作。

但是经过上面的原理分析可知,dubbo是在过滤器链中使用GenericFilter完成泛化调用请求上下文到目标方法请求上下文的替换的,后续的调用和接收正常dubbo请求逻辑一致。

虽然dubbo使用GenericFilter完成了泛化调用到普通dubbo请求的转换,但是dubbo还需要处理服务实现类目标方法的调用逻辑,这里需要首先获取到服务实现类,然后根据方法名调用目标方法。

这里又回到了上面我们讨论过的问题,如何针对反射调用方法进行优化,经过上面的讨论我们也知道了dubbo自研了一套简易的cglib工具,即JavassistProxyFactory。

这部分的实现理念上面已经论述过了,这里不再多讲。

上面我们在讲泛化调用消费端原理时,也简单过了一遍代理类的创建流程,但是当时只讲了使用JdkProxyFactory创建代理的过程,这里再简单扩展一下使用dubbo提供的JavassistProxyFactory创建的代理对象特点:

  • dubbo提供的JavassistProxyFactory创建的代理对象继承了Wrapper接口
// 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创建代理对象,并进行方法调用演示:

    @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. 下载Arthas: curl -O https://arthas.aliyun.com/arthas-boot.jar
  2. 启动测试用例,并在测试方法结尾调用System.in.read()方法挂起当前线程
  3. 启动Arthas: java -jar arthas-boot.jar
    在这里插入图片描述
  4. 模糊搜索所有dubbo生成的代理类

在这里插入图片描述
5. 查看对应代理类的完整代码

在这里插入图片描述

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官网提供的源码解析部分,综合个人理解,如有错误,欢迎评论区指出,或者私信与我讨论。

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值