常规操作篇
上文我们主要用了三个注解@EnableDubbo、@DubboService、@DubboReference
@EnableDubbo可以基本不用理,就是个启动注解
主要用另外两个:
@DubboService:提供服务的注解
@DubboReference:调用服务的注解
如果把dubbo看做是一个容器,那前者就是存入设置好的服务,后者就是要根据配置取出对应的服务
回顾一下上文结构:
服务分组
使用场景
同一个接口针对不同的业务场景、不同的使用需求或者不同的功能模块等场景,可使用服务分组来区分不同的实现方式。同时,这些不同实现所提供的服务是可并存的,也支持互相调用。
大白话:同一个接口可以有不同的实现,类似RocketMq消息中的tag,可以通过分组来确定调用的具体某种实现
实现
// 提供者注解 加上group参数
@DubboService(group = "test1")
// 调用者注解 就需要加上group参数来选择调用具体的哪个服务
@DubboReference(check = false,group = "test1")
看一下服务名就知道了,暴露的服务也会打上对应的参数,如果不带条件获取是找不到服务的哦
服务版本
使用场景
同一个接口可能存在多个版本,每个版本都有不同的逻辑,比如:接口升级从v1升级到了v2,那v1任然有服务在使用不可能直接下架,所以这时候需要v1和v2两个版本共存
实现
// 提供者注解 加上version 参数
@DubboService(group = "test1", version = "v1")
// 调用者注解 就需要加上version参数来选择调用具体的哪个服务
@DubboReference(check = false,group = "test1", version = "v1")
通常情况下需要group和version同时使用才能确定唯一的服务
参数传递
使用场景
在系统间调用时,想传递接口定义之外的一些参数怎么办?就比如一些上下文信息、用户凭证等等
实现
主要是利用RpcContext(完成下面一次远程调用会被清空,即多次远程调用要多次设置)
RpcContext分为(ServerContext、ClientAttachment、ServerAttachment 、ServiceContext)
它们分别承担了不同的职责:
- ServiceContext:在 Dubbo 内部使用,用于传递调用链路上的参数信息,如 invoker 对象等
- ClientAttachment:在 Client 端使用,往 ClientAttachment 中写入的参数将被传递到 Server 端
- ServerAttachment:在 Server 端使用,从 ServerAttachment 中读取的参数是从 Client 中传递过来的
- ServerContext:在 Client 端和 Server 端使用,用于从 Server 端回传 Client 端使用,Server 端写入到 ServerContext 的参数在调用结束后可以在 Client 端的 ServerContext 获取到
调用者: 修改原来的ConsumerTestController
@RestController
@Slf4j
public class ConsumerTestController {
@DubboReference(check = false,group = "test1", version = "v1")
private TestService testService;
@GetMapping("/test")
public String test(String message){
// 调用前给提供者传递参数
RpcContext.getContext().setObjectAttachment("clientKey","server 你好!");
String test = testService.test(message);
// 调用后获取提供者给我的参数
Map<String, Object> serverAttachment = RpcContext.getServerContext().getObjectAttachments();
log.info("ContextTask clientAttachment: " + JSON.toJSON(serverAttachment));
return test;
}
}
提供者: 修改原来的ProducerTestService
@Component
@DubboService(group = "test1", version = "v1")
@Slf4j
public class ProducerTestService implements TestService {
public String test(String message) {
// 接收调用者传递过来的参数
Map<String, Object> clientAttachments = RpcContext.getContext().getObjectAttachments();
log.info("ContextService clientAttachments:" + JSON.toJSONString(clientAttachments));
// 给调用者传递参数
RpcContext.getServerContext().setAttachment("serverKey","client 你好!");
// 返回
return message;
}
}
泛化调用
使用场景
泛化调用(客户端泛化调用)是指在调用方没有服务方提供的 API(SDK),只知道服务的接口的全限定类名和方法名的情况下,对服务方进行调用,并且可以正常拿到调用结果。
实现
调用者: 修改原来的ConsumerTestController ,新增一个泛化调用接口
@GetMapping("/special")
public String special(String message){
// 调用前给提供者传递参数
RpcContext.getContext().setObjectAttachment("clientKey","server 你好!");
// 泛化调用 注意没有用上面引入的 testService
GenericService genericService = buildGenericService("cn.colins.api.TestService","test1","v1");
//传入需要调用的方法名、参数类型列表、参数值列表
Object result = genericService.$invoke("test", new String[]{"java.lang.String"}, new Object[]{message});
System.out.println("GenericTask Response: " + JSON.toJSONString(result));
// 调用后获取提供者给我的参数
Map<String, Object> serverAttachment = RpcContext.getServerContext().getObjectAttachments();
log.info("ContextTask clientAttachment: " + JSON.toJSON(serverAttachment));
return result.toString();
}
private GenericService buildGenericService(String interfaceClass, String group, String version) {
ReferenceConfig<GenericService> reference = new ReferenceConfig<GenericService>();
reference.setInterface(interfaceClass);
reference.setVersion(version);
//开启泛化调用
reference.setGeneric("true");
reference.setTimeout(30000);
reference.setGroup(group);
ReferenceConfigCache cache = ReferenceConfigCache.getCache();
try {
return cache.get(reference);
} catch (Exception e) {
throw new RuntimeException(e.getMessage());
}
}
参数校验
使用场景
这个就不用多说吧,就是用来校验参数的
实现
参数验证功能是基于 JSR303 实现的,用户只需标识 JSR303 标准的验证 annotation ,我们在API模块加入校验依赖 (TestService接口模块)
<!-- Java Validation API -->
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>2.0.1.Final</version>
</dependency>
<!-- Hibernate Validator -->
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.0.17.Final</version>
</dependency>
在接口方法上加入校验注解
public interface TestService {
String test(@NotNull(message = "消息不能为空") String message);
}
然后在提供者和调用者注解上加入 validation="true"即可
// 提供者注解 加上validation 参数
@DubboService(group = "test1", version = "v1",validation = "true")
// 调用者注解 就需要加上validation 参数
@DubboReference(check = false,group = "test1", version = "v1",validation = "true")
实际测试两个注解其中一个加了就能生效,在调用方报错
异常处理,还有这个校验更多的使用不用介绍了吧,平时用的也挺多的
只订阅
使用场景
为方便开发测试,经常会在线下共用一个所有服务可用的注册中心,这时,如果一个正在开发中的服务提供者注册,可能会影响调用者不能正常运行。可以让服务提供者开发方,只订阅服务(开发的服务可能依赖其它服务),而不注册正在开发的服务,通过直连测试正在开发的服务。
实现
// 提供者注解 register=false 代表不注册
@DubboService(group = "test1", version = "v1",validation = "true",register = false)
服务提供者就找不到了
延迟暴露
这个效果就是等到spring容器初始化完后,延迟多久才将服务注册到注册中心
// 提供者注解 delay延迟时间 单位:毫秒
@DubboService(group = "test1", version = "v1",validation = "true",delay = 10000)
服务端异步回调
使用场景
回调函数通知客户端执行结果,或发送通知,在方法执行时间比较长时,类似异步调用,审批工作流中回调客户端审批结果。
先看一下效果吧,可能这样说不太理解,其实和平时的本地回调差不多
实现
先在API模块新增两个接口CallbackService 和 CallbackListener
public interface CallbackListener {
void changed(String msg);
}
public interface CallbackService {
String addListener(String key, CallbackListener listener);
}
服务提供者
// 代表给addListener 方法添加一个类型为cn.colins.api.CallbackListener 的回调
@DubboService(methods = {
@Method(name = "addListener", arguments = { @Argument(callback = true, type = "cn.colins.api.CallbackListener") })
})
public class CallbackTestServiceImpl implements CallbackService {
@Override
public String addListener(String key, CallbackListener listener) {
// 异步处理
new Thread(()->{
listener.changed(getChanged(key)); // 异步处理
}).start();
// 直接返回
return key;
}
private String getChanged(String key) {
// 逻辑处理。。。。。完后返回
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "处理了5s返回结果";
}
}
服务调用者
// 新增一个接口正常调用就好了
@GetMapping("/callbackTest")
public String callbackTest(){
String test = callbackService.addListener("test", new CallbackListener() {
@Override
public void changed(String msg) {
log.info("callback msg : {}", msg);
}
});
log.info("直接调用返回的 msg : {}", test);
return test;
}
效果就是先直接返回了,然后5s后拿到服务端处理后的结果
整体结构现如下:
多协议复用
使用场景
可以与不同的系统兼容,试想同一个接口你在内部采用dubbo协议通信,这时候又需要暴露出去与其他不同协议的系统对接,难不成每种协议都写一个接口?而且除此之外它还有其他好处
- 改进的性能:不同的协议可能具有不同的性能特征,这取决于传输的数据量和网络条件等因素。通过使用多种协议,可以根据您的性能要求选择最适合给定情况的协议。
- 安全性:一些协议可能提供比其他协议更好的安全特性。HTTPS 协议通过加密传输的数据来提供安全通信,这对于保护敏感数据非常有用。
- 易用性:某些协议在某些情况下可能更易于使用。如果正在构建 Web 应用程序并希望与远程服务集成,使用 HTTP 协议可能比使用需要更复杂设置的协议更方便
实现
这里以同时配置dubbo和rest协议为例,修改dubbo-producer项目
配置文件
dubbo:
registry:
address: nacos://returnac.cn:8848
group: DUBBO_TEST
username:
password:
application:
name: dubbo-producer
scan:
base-packages: cn.colins.api
protocols:
dubbo:
name: dubbo
port: 22223
rest:
name: rest
port: 22224
# 使用内置服务器 还可以用其他类型的容器
server: tomcat
依赖添加
因为rest协议是基于标准的 Java REST API——JAX-RS 2.0(Java API for RESTful Web Services 的简写)实现的 REST 调用支持,所以要引入相关依赖
<!--dubbo支持rest-->
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-rpc-rest</artifactId>
<version>2.7.12</version>
</dependency>
<dependency>
<groupId>javax.ws.rs</groupId>
<artifactId>javax.ws.rs-api</artifactId>
<version>2.1.1</version>
</dependency>
提供者服务修改
protocol: 代表同时支持的协议
@Path : 代表路径
@Produces: 代表返回设置
关于JAX-RS相关的配置就不做多的说明了
@Component
@DubboService(group = "test1", version = "v1",validation = "true",protocol = {"dubbo","rest"})
@Path("rest")
@Produces({"application/json; charset=UTF-8", "text/xml; charset=UTF-8"})
public class ProducerTestService implements TestService {
private final static Logger log= LoggerFactory.getLogger(ProducerTestService.class);
@GET()
@Path("test")
@Override
public String test(@QueryParam("message") String message) {
// 接收调用者传递过来的参数
Map<String, Object> clientAttachments = RpcContext.getContext().getObjectAttachments();
log.info("ContextService clientAttachments:" + JSON.toJSONString(clientAttachments));
// 给调用者传递参数
RpcContext.getServerContext().setAttachment("serverKey","client 你好!");
// 返回
return message;
}
}
调用者修改
// 因为提供者采用了多协议,所以这里也需要配置protocol指定具体的协议
@DubboReference(check = false,group = "test1", version = "v1",validation = "true",protocol = "dubbo")
测试结果
内部的dubbo协议远程调用依旧正常
而我们直接http请求也ok了,注意rest配置的是22224端口
再看看nacos控制台,你会发现实例数变成了两个,区别就是端口和协议不同
多注册中心
使用场景
Dubbo 支持同一服务向多注册中心同时注册,或者不同服务分别注册到不同的注册中心上去,甚至可以同时引用注册在不同注册中心上的同名服务。另外,注册中心是支持自定义扩展的,有以下好处:
- 高可用:多个注册服务器确保即使其中一个注册服务器出现故障,服务仍然可用。
- 负载:同时访问大量服务,使用多个注册服务器帮助在多个服务器之间分配负载提高系统的整体性能和可扩展性。
- 区域:不同地理位置的服务,使用多个注册服务器来确保根据其位置注册和发现服务减少请求需要传输的距离来帮助提高系统的性能和可靠性。
- 安全:使用多个注册服务器。期望将一台注册服务器用于内部服务,另一台用于外部服务,以确保只有授权的客户端才能访问您的内部服务
实现
在配置文件中配置:
dubbo:
# 单个注册中心
# registry:
# address: nacos://IP:PORT
# group: DUBBO_TEST
registries:
# 注册中心ID 别名 不能重复
center1:
address: nacos://IP:PORT
group: DUBBO_TEST
center2:
address: nacos://IP:PORT
group: DUBBO_TEST
然后再注解中选择需要注册到哪个注册中心
// 提供者注解 加上registry参数 选择需要注册到哪个注册中心
@DubboService(registry= "center1")
// 调用者注解 加上registry参数 选择需要注册到哪个注册中心
@DubboReference(check = false,registry = "center1")
// 也可以同时注册到多个注册中心,用,隔开: registry = "center1,center2"
本地存根
使用场景
远程服务后,客户端通常只剩下接口,而实现全在服务器端,但提供方有些时候想在客户端也执行部分逻辑,说白了就是在调用方调用方法的进行了一层包装
实现
我们在调用方,也就是consumer下新建一个文件夹service
新建MyTestService实现TestService接口(就是我们需要远程调用的接口)
注意: 构造方法一定要有,而且参数就是接口对象,下面实现的方法则是最终会调用的方法
public class MyTestService implements TestService {
private final static Logger log= LoggerFactory.getLogger(MyTestService.class);
private TestService testService;
public MyTestService(TestService testService){
this.testService=testService;
}
@Override
public String test(String message) {
// 此代码在客户端执行, 你可以在客户端做ThreadLocal本地缓存,或预先验证参数是否合法,等等
log.info("存根测试...");
try {
return testService.test(message);
} catch (Exception e) {
// 你可以容错,可以做任何AOP拦截事项
return "容错数据";
}
}
}
然后再调用者注解上加上stub参数,配置实现类为全路径类名
@DubboReference(check = false,stub = "cn.colins.consumer.service.MyTestService")
结果: 最终是走的我们实现类中的方法