一、背景
我在一个项目中,使用直连式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生成。