dubbo 参数回调


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

消费端执行任务,不在服务端执行

    

         

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值