dubbo 参数回调
官网:https://dubbo.apache.org/zh/docs/advanced/callback-parameter/
参数回调
参数回调:服务端反向调用消费端方法
服务端:服务接口
public interface CallbackService {
void addListener(String key, CallbackListener listener);
}
服务端:回调接口,该接口回调客户端操作
public interface CallbackListener {
void changed(String msg);
}
服务端:服务接口实现类
public class CallbackServiceImpl implements CallbackService {
public void addListener(String key, CallbackListener listener) {
listener.changed(getChanged(key)); //调用客户端处理逻辑
}
private String getChanged(String key) {
return "Changed: " + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());
}
}
服务端:配置回调接口
<bean id="callbackService" class="com.callback.impl.CallbackServiceImpl" />
<dubbo:service interface="com.callback.CallbackService" ref="callbackService" connections="1" callbacks="1000">
<dubbo:method name="addListener">
<!--index从0开始计数-->
<dubbo:argument index="1" callback="true" />
<!--也可以通过指定类型的方式-->
<!--<dubbo:argument type="com.demo.CallbackListener" callback="true" />-->
</dubbo:method>
</dubbo:service>
消费端:提供回调接口实现
# 注册服务
<dubbo:reference id="callbackService" interface="com.callback.CallbackService" />
# 服务调用时提供接口实现
callbackService.addListener("foo.bar", new CallbackListener(){
public void changed(String msg) {
System.out.println("callback1:" + msg);
}
});
实现原理
参数回调处理过程
# 消费端
消费端启动时会获取服务元数据,由于服务端配置了回调参数,客户端会获取回调信息;
消费端在发起请求时,异步执行以下操作:
* 将回调参数对象的内存id存储在attachment中,key为sys_callback_arg-index(index
为实际索引值,如0)
* 暴露回调接口的dubbo协议服务(复用消费端、客户端建立的tcp连接)
sys_callback_arg-index会随请求一起发送给服务端;
# 服务端
服务度端处理回调参数时,会将sys_callback_arg-index的值传递给消费端,key为:callback.service.instid
# 消费端
消费端接到请求后,根据callback.service.instid查找对应的回调对象,执行对应操作
CallbackServiceCodec:客户端回调参数对象暴露
public class CallbackServiceCodec {
public Object encodeInvocationArgument(Channel channel, RpcInvocation inv, int paraIndex) throws IOException {
//将回调参数对象内存id存储在attachment中
// get URL directly
URL url = inv.getInvoker() == null ? null : inv.getInvoker().getUrl();
byte callbackStatus = isCallBack(url, inv.getProtocolServiceKey(), inv.getMethodName(), paraIndex);
Object[] args = inv.getArguments();
Class<?>[] pts = inv.getParameterTypes();
switch (callbackStatus) {
case CallbackServiceCodec.CALLBACK_CREATE: //新建回调参数对象
inv.setAttachment(INV_ATT_CALLBACK_KEY + paraIndex, exportOrUnexportCallbackService(channel, inv, url, pts[paraIndex], args[paraIndex], true));
//key:sys_callback_arg- paraIndex
//value:回调参数对象内存id(System.identityHashCode(args[paraIndex]))
//exportOrUnexportCallbackService暴露或者卸载回调参数服务
return null;
case CallbackServiceCodec.CALLBACK_DESTROY:
inv.setAttachment(INV_ATT_CALLBACK_KEY + paraIndex, exportOrUnexportCallbackService(channel, inv, url, pts[paraIndex], args[paraIndex], false));
return null;
default:
return args[paraIndex];
}
}
/**
* export or unexport callback service on client side
//在客户端暴露或者卸载回调服务
*
* @param channel
* @param url
* @param clazz
* @param inst
* @param export
* @throws IOException
*/
@SuppressWarnings({"unchecked", "rawtypes"})
private String exportOrUnexportCallbackService(Channel channel, RpcInvocation inv, URL url, Class clazz, Object inst, Boolean export) throws IOException {
int instid = System.identityHashCode(inst);
Map<String, String> params = new HashMap<>(3);
// no need to new client again
params.put(IS_SERVER_KEY, Boolean.FALSE.toString());
// mark it's a callback, for troubleshooting
params.put(IS_CALLBACK_SERVICE, Boolean.TRUE.toString());
String group = (url == null ? null : url.getGroup());
if (group != null && group.length() > 0) {
params.put(GROUP_KEY, group);
}
// add method, for verifying against method, automatic fallback (see dubbo protocol)
params.put(METHODS_KEY, StringUtils.join(Wrapper.getWrapper(clazz).getDeclaredMethodNames(), ","));
Map<String, String> tmpMap = new HashMap<>();
if (url != null) {
Map<String, String> parameters = url.getParameters();
if (parameters != null && !parameters.isEmpty()) {
tmpMap.putAll(parameters);
}
}
tmpMap.putAll(params);
tmpMap.remove(VERSION_KEY);// doesn't need to distinguish version for callback
tmpMap.remove(Constants.BIND_PORT_KEY); //callback doesn't needs bind.port
tmpMap.put(INTERFACE_KEY, clazz.getName());
URL exportUrl = new ServiceConfigURL(DubboProtocol.NAME, channel.getLocalAddress().getAddress().getHostAddress(),
channel.getLocalAddress().getPort(), clazz.getName() + "." + instid, tmpMap);
// no need to generate multiple exporters for different channel in the same JVM, cache key cannot collide.
String cacheKey = getClientSideCallbackServiceCacheKey(instid);
String countKey = getClientSideCountKey(clazz.getName());
if (export) {
// one channel can have multiple callback instances, no need to re-export for different instance.
if (!channel.hasAttribute(cacheKey)) {
if (!isInstancesOverLimit(channel, url, clazz.getName(), instid, false)) {
ModuleModel moduleModel;
if (inv.getServiceModel() == null) {
//TODO should get scope model from url?
moduleModel = ApplicationModel.defaultModel().getDefaultModule();
logger.error("Unable to get Service Model from Invocation. Please check if your invocation failed! " +
"This error only happen in UT cases! Invocation:" + inv);
} else {
moduleModel = inv.getServiceModel().getModuleModel();
}
ServiceDescriptor serviceDescriptor = moduleModel.getServiceRepository().registerService(clazz);
ServiceMetadata serviceMetadata = new ServiceMetadata(clazz.getName() + "." + instid, exportUrl.getGroup(), exportUrl.getVersion(), clazz);
String serviceKey = BaseServiceMetadata.buildServiceKey(exportUrl.getPath(), group, exportUrl.getVersion());
ProviderModel providerModel = new ProviderModel(serviceKey, inst, serviceDescriptor, null, moduleModel, serviceMetadata);
moduleModel.getServiceRepository().registerProvider(providerModel);
exportUrl = exportUrl.setScopeModel(moduleModel);
exportUrl = exportUrl.setServiceModel(providerModel);
Invoker<?> invoker = proxyFactory.getInvoker(inst, clazz, exportUrl);
// should destroy resource?
Exporter<?> exporter = protocolSPI.export(invoker);
// this is used for tracing if instid has published service or not.
channel.setAttribute(cacheKey, exporter);
logger.info("Export a callback service :" + exportUrl + ", on " + channel + ", url is: " + url);
increaseInstanceCount(channel, countKey);
}
}
} else {
if (channel.hasAttribute(cacheKey)) {
Exporter<?> exporter = (Exporter<?>) channel.getAttribute(cacheKey);
exporter.unexport();
channel.removeAttribute(cacheKey);
decreaseInstanceCount(channel, countKey);
}
}
return String.valueOf(instid);
}
InvokerInvocationHandler:服务端参数回调请求调用
public class InvokerInvocationHandler implements InvocationHandler {
private static final Logger logger = LoggerFactory.getLogger(InvokerInvocationHandler.class);
private final Invoker<?> invoker;
private ServiceModel serviceModel;
private URL url;
private String protocolServiceKey;
public static Field stackTraceField;
static {
try {
stackTraceField = Throwable.class.getDeclaredField("stackTrace");
stackTraceField.setAccessible(true);
} catch (NoSuchFieldException e) {
// ignore
}
}
public InvokerInvocationHandler(Invoker<?> handler) {
this.invoker = handler;
this.url = invoker.getUrl();
this.protocolServiceKey = this.url.getProtocolServiceKey();
this.serviceModel = this.url.getServiceModel();
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//回调参数请求调用
if (method.getDeclaringClass() == Object.class) {
return method.invoke(invoker, args);
}
String methodName = method.getName();
Class<?>[] parameterTypes = method.getParameterTypes();
if (parameterTypes.length == 0) {
if ("toString".equals(methodName)) {
return invoker.toString();
} else if ("$destroy".equals(methodName)) {
invoker.destroy();
return null;
} else if ("hashCode".equals(methodName)) {
return invoker.hashCode();
}
} else if (parameterTypes.length == 1 && "equals".equals(methodName)) {
return invoker.equals(args[0]);
}
RpcInvocation rpcInvocation = new RpcInvocation(serviceModel, method, invoker.getInterface().getName(), protocolServiceKey, args);
String serviceKey = url.getServiceKey();
rpcInvocation.setTargetServiceUniqueName(serviceKey);
// invoker.getUrl() returns consumer url.
RpcServiceContext.setRpcContext(url); //设置消费地址
if (serviceModel instanceof ConsumerModel) {
rpcInvocation.put(Constants.CONSUMER_MODEL, serviceModel);
rpcInvocation.put(Constants.METHOD_MODEL, ((ConsumerModel) serviceModel).getMethodModel(method));
}
return invoker.invoke(rpcInvocation).recreate();
}
}
ChannelWrapper:服务端发起回调请求,将回调参数对象内存id添加到请求中
class ChannelWrappedInvoker<T> extends AbstractInvoker<T> {
private final Channel channel;
private final String serviceKey; //回调参数对象内存id
private final ExchangeClient currentClient;
ChannelWrappedInvoker(Class<T> serviceType, Channel channel, URL url, String serviceKey) {
super(serviceType, url, new String[]{GROUP_KEY, TOKEN_KEY});
this.channel = channel;
this.serviceKey = serviceKey;
this.currentClient = new HeaderExchangeClient(new ChannelWrapper(this.channel), false);
}
@Override
protected Result doInvoke(Invocation invocation) throws Throwable {
RpcInvocation inv = (RpcInvocation) invocation;
// use interface's name as service path to export if it's not found on client side
inv.setAttachment(PATH_KEY, getInterface().getName());
inv.setAttachment(CALLBACK_SERVICE_KEY, serviceKey);
//将回调参数对象内存id附加到attachment上,key为:callback.service.instid
try {
if (RpcUtils.isOneway(getUrl(), inv)) { // may have concurrency issue
currentClient.send(inv, getUrl().getMethodParameter(invocation.getMethodName(), SENT_KEY, false));
return AsyncRpcResult.newDefaultAsyncResult(invocation);
} else {
CompletableFuture<AppResponse> appResponseFuture = currentClient.request(inv).thenApply(obj -> (AppResponse) obj);
return new AsyncRpcResult(appResponseFuture, inv);
}
} catch (RpcException e) {
throw e;
} catch (TimeoutException e) {
throw new RpcException(RpcException.TIMEOUT_EXCEPTION, e.getMessage(), e);
} catch (RemotingException e) {
throw new RpcException(RpcException.NETWORK_EXCEPTION, e.getMessage(), e);
} catch (Throwable e) { // here is non-biz exception, wrap it.
throw new RpcException(e.getMessage(), e);
}
}
HeaderExchangeHandler:消费端处理回调请求
public class HeaderExchangeHandler implements ChannelHandlerDelegate {
void handleRequest(final ExchangeChannel channel, Request req) throws RemotingException {
Response res = new Response(req.getId(), req.getVersion());
if (req.isBroken()) {
Object data = req.getData();
String msg;
if (data == null) {
msg = null;
} else if (data instanceof Throwable) {
msg = StringUtils.toString((Throwable) data);
} else {
msg = data.toString();
}
res.setErrorMessage("Fail to decode request due to: " + msg);
res.setStatus(Response.BAD_REQUEST);
channel.send(res);
return;
}
// find handler by message class.
Object msg = req.getData(); //获取请求数据
try {
CompletionStage<Object> future = handler.reply(channel, msg); //处理请求
future.whenComplete((appResult, t) -> {
try {
if (t == null) {
res.setStatus(Response.OK);
res.setResult(appResult);
} else {
res.setStatus(Response.SERVICE_ERROR);
res.setErrorMessage(StringUtils.toString(t));
}
channel.send(res);
} catch (RemotingException e) {
logger.warn("Send result to consumer failed, channel is " + channel + ", msg is " + e);
}
});
} catch (Throwable e) {
res.setStatus(Response.SERVICE_ERROR);
res.setErrorMessage(StringUtils.toString(e));
channel.send(res);
}
}
JavassistProxyFactory:创建回调参数对象wrapper对象,调用对应方法
public class JavassistProxyFactory extends AbstractProxyFactory {
@Override
@SuppressWarnings("unchecked")
public <T> T getProxy(Invoker<T> invoker, Class<?>[] interfaces) {
return (T) Proxy.getProxy(interfaces).newInstance(new InvokerInvocationHandler(invoker));
}
@Override
public <T> Invoker<T> getInvoker(T proxy, Class<T> type, URL url) {
// TODO Wrapper cannot handle this scenario correctly: the classname contains '$'
final Wrapper wrapper = Wrapper.getWrapper(proxy.getClass().getName().indexOf('$') < 0 ? proxy.getClass() : type);
return new AbstractProxyInvoker<T>(proxy, type, url) {
@Override
protected Object doInvoke(T proxy, String methodName,
Class<?>[] parameterTypes,
Object[] arguments) throws Throwable {
return wrapper.invokeMethod(proxy, methodName, parameterTypes, arguments);
}
};
}
}
使用示例
************
服务端
application.yml
dubbo:
application:
name: dubbo-provider
#register-mode: instance
registry:
address: localhost:2181
protocol: zookeeper
group: dubbo
protocol:
name: dubbo
#port: 20880
HelloService
public interface HelloService {
String hello(CustomListener listener);
}
CustomListener
public interface CustomListener {
void listen(LocalDateTime localDateTime);
}
HelloServiceImpl
public class HelloServiceImpl implements HelloService {
@Override
public String hello(CustomListener listener) {
listener.listen(LocalDateTime.now());
return "hello";
}
}
DemoApplication
@EnableDubbo
@SpringBootApplication
@ImportResource("classpath:dubbo/provider.xml") //导入配置文件
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
dubbo/provider.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:dubbo="http://dubbo.apache.org/schema/dubbo"
xmlns="http://www.springframework.org/schema/beans"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.3.xsd
http://dubbo.apache.org/schema/dubbo http://dubbo.apache.org/schema/dubbo/dubbo.xsd">
<bean id="helloServiceImpl" class="com.example.demo.service.impl.HelloServiceImpl"/>
<dubbo:service id="helloService" ref="helloServiceImpl"
interface="com.example.demo.service.HelloService">
<dubbo:method name="hello">
<dubbo:argument index="0" callback="true"/>
</dubbo:method>
</dubbo:service>
</beans>
************
消费端
application.yml
dubbo:
application:
name: dubbo-consumer
registry:
protocol: zookeeper
address: localhost:2181
group: dubbo
#register-mode: instance
protocol:
name: dubbo
#port: 20880
server:
port: 8081
HelloService
public interface HelloService {
String hello(CustomListener listener);
}
CustomListener
public interface CustomListener {
void listen(LocalDateTime localDateTime);
}
HelloController
@RestController
public class HelloController {
@DubboReference
private HelloService helloService;
@RequestMapping("/hello")
public String hello(){
return helloService.hello(localDateTime -> System.out.println("当前时间为:"+localDateTime));
//调用服务时提供回调接口实现
}
}
************
使用测试
localhost:8080/hello,控制台输出:
2022-02-24 10:59:22.527 INFO 1436 --- [ main] com.example.demo.DemoApplication : Started DemoApplication in 2.839 seconds (JVM running for 3.403)
2022-02-24 10:59:25.232 INFO 1436 --- [nio-8081-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring DispatcherServlet 'dispatcherServlet'
2022-02-24 10:59:25.233 INFO 1436 --- [nio-8081-exec-1] o.s.web.servlet.DispatcherServlet : Initializing Servlet 'dispatcherServlet'
2022-02-24 10:59:25.233 INFO 1436 --- [nio-8081-exec-1] o.s.web.servlet.DispatcherServlet : Completed initialization in 0 ms
当前时间为:2022-02-24T10:59:25.281622
消费端执行任务,不在服务端执行