一种基于直连式Dubbo的动态远程服务调用方法

一、背景

我在一个项目中,使用直连式Dubbo作为各节点之间的通信方式。遇到这样一个问题——即当一个消费者面对多个生产者,要根据条件动态选择其中一个生产者去调用服务时,应该如何实现?还有这样一个问题,在面对同一个生产者时,我如何根据条件动态地去调用不同的api呢?

这个问题显而易见地解决方法是,把可能用到的服务提前注入,写一个switch语句判断究竟要调用哪一个服务。可是,如果现在我们的场景是在一个P2P网络中,随时都会有服务的加入也会有服务的退出,那么就不可能在开发阶段就把所有可能用到的服务都在代码里体现出来,需要一种动态远程服务调用的方式。

我正在做的一个项目,将多个应用连接起来,形成一个对等网络。每个应用各自维护一张本地路由表,各应用之间通过本地路由表+直连式Dubbo的方式实现通信。我希望某一个应用能够通过ip地址、接口名称调用网络中所有应用的所有功能,那么势必需要实现一种动态RPC。

接下来,我将展现一种基于直连式Dubbo的动态远程服务调用方法:

二、代码实现

2.1 创建项目

2.2.1 新建项目

环境版本:jdk1.8, springboot2.4.2, dubbo2.7.1

 首先新建一个springboot项目,删掉多余的东西。

然后,在dependencyManagement里指定一下dubbo版本,我使用的式2.7.1

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.apache.dubbo</groupId>
            <artifactId>dubbo</artifactId>
            <version>2.7.1</version>
            </dependency>
        <dependency>
            <groupId>org.apache.dubbo</groupId>
            <artifactId>dubbo-spring-boot-starter</artifactId>
            <version>2.7.1</version>
        </dependency>
    </dependencies>
</dependencyManagement>

2.2.2 api模块

新建一个module

api模块用于定义远程服务调用所需要的接口,可以定义一个测试接口,HelloService

package com.example.api.service;

public interface HelloService {
    String sayHello(String name);
}

2.2.3 provider模块

provider模块需要实现api中的接口,为了体现出1对多的关系,这里我们定义两个服务:provider-cat和provider-dog

生产者需要在配置文件中配置dubbo信息,实现HelloService接口。

cat:

首先定义好启动类,测试能够成功运行

其次,pom.xml中添加dubbo相关依赖以及api模块的依赖

<dependencies>
    <!-- dubbo -->
    <dependency>
        <groupId>org.apache.dubbo</groupId>
        <artifactId>dubbo</artifactId>
    </dependency>
    <dependency>
        <groupId>org.apache.dubbo</groupId>
        <artifactId>dubbo-spring-boot-starter</artifactId>
    </dependency>
    <dependency>
        <groupId>com.example</groupId>
        <artifactId>api</artifactId>
        <version>0.0.1-SNAPSHOT</version>
        <scope>compile</scope>
    </dependency>
</dependencies>

配置文件application.yml里配置dubbo相关信息

server:
  port: 8001
dubbo:
  registry:
    address: N/A  # 不使用注册中心
  application:
    name: cat
  scan:
    base-packages: com.example.cat.impl  # 扫描包下的服务
  protocol:
    port: 20881  # dubbo端口号

实现HelloService接口,注意,这里的@Service注解由dubbo提供(新版本的注解已经换成了@DubboService,防止歧义)。

dog:

<dependencies>
    <!-- dubbo -->
    <dependency>
        <groupId>org.apache.dubbo</groupId>
        <artifactId>dubbo</artifactId>
    </dependency>
    <dependency>
        <groupId>org.apache.dubbo</groupId>
        <artifactId>dubbo-spring-boot-starter</artifactId>
    </dependency>
    <dependency>
        <groupId>com.example</groupId>
        <artifactId>api</artifactId>
        <version>0.0.1-SNAPSHOT</version>
        <scope>compile</scope>
    </dependency>
</dependencies>
server:
  port: 8002
dubbo:
  registry:
    address: N/A  # 不使用注册中心
  application:
    name: dog
  scan:
    base-packages: com.example.dog.impl  # 扫描包下的服务
  protocol:
    port: 20882  # dubbo端口号

2.2.4 consumer模块

<dependencies>
    <!-- dubbo -->
    <dependency>
        <groupId>org.apache.dubbo</groupId>
        <artifactId>dubbo</artifactId>
    </dependency>
    <dependency>
        <groupId>org.apache.dubbo</groupId>
        <artifactId>dubbo-spring-boot-starter</artifactId>
    </dependency>
    <dependency>
        <groupId>com.example</groupId>
        <artifactId>api</artifactId>
        <version>0.0.1-SNAPSHOT</version>
        <scope>compile</scope>
    </dependency>
</dependencies>
server:
  port: 8003
dubbo:
  application:
    name: consumer

写一个controller用于测试

package com.example.consumer.controller;

import com.example.api.service.HelloService;
import org.apache.dubbo.config.annotation.Reference;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/consumer")
public class ConsumerController {
    @Reference(url = "dubbo://localhost:20881")
    private HelloService helloService;

    @GetMapping("/hello")
    public String hello() {
        return helloService.sayHello("文明冲浪");
    }
}

成功调用了cat服务的sayHello()方法。

2.2 根据IP地址动态指定生产者

2.2.1 使用ReferenceConfig获取生产者服务引用

在consumer下创建一个RpcHelper

package com.example.consumer.rpc;

import org.apache.dubbo.config.ReferenceConfig;
import org.springframework.stereotype.Component;

@Component
public class RpcHelper {
    /**
     * 根据地址,获取生产者服务
     * @param address IP地址
     * @param clazz 类型信息
     * @return 生产者服务
     * @param <T> 服务类型
     */
    public <T> T getServiceByAddress(String address, Class<T> clazz) {
        // 构造直连URL
        String url = "dubbo://" + address;
        // 创建引用配置
        ReferenceConfig<T> referenceConfig = new ReferenceConfig<>();
        referenceConfig.setInterface(clazz);
        referenceConfig.setUrl(url); // 设置直连URL
        // 获取服务引用
        return referenceConfig.get();
    }
}

在controller中,使用此方法,动态指定生产者

package com.example.consumer.controller;

import com.example.api.service.HelloService;
import com.example.consumer.rpc.RpcHelper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/consumer")
public class ConsumerController {

    @Autowired
    private RpcHelper rpcHelper;

    @GetMapping("/hello")
    public String hello(@RequestParam("id") Integer id) {
        String address1 = "localhost:20881";
        String address2 = "localhost:20882";
        if (1 == id) {
            return rpcHelper.getServiceByAddress(address1, HelloService.class).sayHello("文明冲浪");
        }
        if (2 == id) {
            return rpcHelper.getServiceByAddress(address2, HelloService.class).sayHello("文明冲浪");
        }
        return "";
    }
}

测试:当id为1时,调用了cat的方法,当id为2时,调用了dog的方法。

2.3 根据beanName与methodName动态调用功能

2.2节展示了如何条件选择不同的生产者调用功能,但是假如存在这样一种情况:消费者的代码写好了不想改了,而此时生产者又增加了新接口。这时想要调用生产者的新功能是不是就必须修改代码再重新打包部署呢?Spring提供了根据名称从上下文中获取Bean,并通过反射调用方法的功能。

在生产者侧,写一个HandlerHelper,这里以cat为例

package com.example.cat.handler;

import com.sun.istack.internal.NotNull;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;

@Component
public class HandlerHelper implements ApplicationContextAware {
    private ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(@NotNull ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

    public Object invokeMethod(String beanName, String methodName, Object... args) throws Exception {
        Object bean = applicationContext.getBean(beanName);
        Class<?> beanClass = bean.getClass();
        Class<?>[] parameterTypes = new Class[args.length];
        for (int i = 0; i < args.length; i++) {
            parameterTypes[i] = args[i].getClass();
        }
        Method method = beanClass.getMethod(methodName, parameterTypes);
        return method.invoke(bean, args);
    }
}

在cat里定义多个service

package com.example.cat.service;

import org.springframework.stereotype.Service;

@Service("catService")
public class CatService {
    public String catName(String name) {
        return "the cat's name is " + name;
    }
}
package com.example.cat.service;

import org.springframework.stereotype.Service;

@Service("infoService")
public class InfoService {
    public String getInfo(Integer id) {
        return "get information by id: " + id;
    }
}

api模块定义HandlerService接口以及入参

package com.example.api.service;

import com.example.api.model.HandlerParam;

public interface HandlerService {
    Object handle(HandlerParam param);
}
package com.example.api.model;

import lombok.Data;

@Data
public class HandlerParam implements Serializable {
    private String serviceName;
    private String methodName;
    private Object[] params;
}

生产者cat要实现HandlerService接口

package com.example.cat.impl;

import com.example.api.model.HandlerParam;
import com.example.api.service.HandlerService;
import com.example.cat.handler.HandlerHelper;
import org.apache.dubbo.config.annotation.Service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Service
@Component
public class HandlerServiceImpl implements HandlerService {

    @Autowired
    private HandlerHelper handlerHelper;

    @Override
    public Object handle(HandlerParam param) {
        try {
            return handlerHelper.invokeMethod(param.getServiceName(),
                    param.getMethodName(),
                    param.getParams());
        } catch (Exception e) {
            System.out.println(e.getMessage());
        }
        return null;
    }
}

消费者里调用:

测试:根据不同的beanName和methodName调用了不同的方法

三、总结

其实上述功能可以通过restTemplate,直接构造各种http请求来实现,还非常简单。但是rpc有其自身优势。

部分代码由AI生成

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值