草庐IT

Tars-Java客户端源码分析

vivo互联网 2023-03-28 原文

一、基本RPC框架简介

在分布式计算中,远程过程调用(Remote Procedure Call,缩写 RPC)允许运行于一台计算机的程序调用另一个地址空间计算机的程序,就像调用本地程序一样,无需额外地为这个交互作用涉及到的代理对象构建、网络协议等进行编程。

一般RPC架构,有至少三种结构,分别为注册中心,服务提供者和服务消费者。如图1.1所示,注册中心提供注册服务和注册信息变更的通知服务,服务提供者运行在服务器来提供服务,服务消费者使用服务提供者的服务。

服务提供者(RPC Server),运行在服务端,提供服务接口定义与服务实现类,并对外暴露服务接口。注册中心(Registry),运行在服务端,负责记录服务提供者的服务对象,并提供远程服务信息的查询服务和变更通知服务。服务消费者(RPC Client),运行在客户端,通过远程代理对象调用远程服务。

(RPC框架基本结构)

1.1 RPC调用流程

如下图所示,描述了RPC的调用流程,其中IDL(Interface Description Language)为接口描述语言,使得在不同平台上运行的程序和用不同语言编写的程序可以相互通信交流。

(RPC调用流程) 1)客户端调用客户端桩模块。该调用是本地过程调用,其中参数以正常方式推入堆栈。

2)客户端桩模块将参数打包到消息中,并进行系统调用以发送消息。打包参数称为编组。

3)客户端的本地操作系统将消息从客户端计算机发送到服务器计算机。

4)服务器计算机上的本地操作系统将传入的数据包传递到服务器桩模块。

5)服务器桩模块从消息中解包出参数。解包参数称为解组。

6)最后,服务器桩模块执行服务器程序流程。回复是沿相反的方向执行相同的步骤。

二、Tars Java客户端设计介绍

Tars Java客户端整体设计与主流的RPC框架基本一致。我们先介绍Tars Java客户端初始化过程。

2.1 Tars Java客户端初始化过程

如图2.1所示,描述了Tars Java的初始化过程。

(Tars Java初始化过程) 1)先出创建一个CommunicatorConfig配置项,命名为communicatorConfig,其中按需设置locator, moduleName, connections等参数。

2)通过上述的CommunicatorConfig配置项,命名为config,那么调用CommunicatorFactory.getInstance().getCommunicator(config),创建一个Communicator对象,命名为communicator。

3)假设objectName="MESSAGE.ControlCenter.Dispatcher",需要生成的代理接口为Dispatcher.class,调用communicator.stringToProxy(objectName, Dispatcher.class)方法来生成代理对象的实现类。

4)在stringToProxy()方法里,首先通过初始化QueryHelper代理对象,调用getServerNodes()方法获取远程服务对象列表,并设置该返回值到communicatorConfig的objectName字段里。具体的代理对象的代码分析,见下文中的“2.3 代理生成”章节。

5)判断在之前调用stringToProxy是否有设置LoadBalance参数,如果没有的话,就生成默认的采用RR轮训算法的DefaultLoadBalance对象。

6)创建TarsProtocolInvoker协议调用对象,其中过程有通过解析communicatorConfig中的objectName和simpleObjectName来获取URL列表,其中一个URL对应一个远程服务对象,TarsProtocolInvoker初始化各个URL对应的ServantClient对象,其中一个URL根据communicatorConfig的connections配置项确认生成多少个ServantClient对象。然后使用ServantClients等参数初始化TarsInvoker对象,并将这些TarsInvoker对象集合设置到TarsProtocolInvoker的allInvokers成员变量中,其中每个URL对应一个TarsInvoker对象。上述分析表明,一个远程服务节点对应一个TarsInvoker对象,一个TarsInvoker对象包含connections个ServantClient对象,对于TCP协议,那么就是一个ServantClient对象对应一个TCP连接。

7)使用api, objName, servantProxyConfig,loadBalance,protocolInvoker, this.communicator参数生成一个实现JDK代理接口InvocationHandler的ObjectProxy对象。

8)生成ObjectProxy对象的同时进行初始化操作,首先会执行loadBalancer.refresh()方法刷新远程服务节点到负载均衡器中便于后续tars远程调用进行路由。

9)然后注册统计信息上报器,其中是上报方法采用JDK的ScheduledThreadPoolExecutor进行定时轮训上报。

10)注册服务列表刷新器,采用的技术方法和上述统计信息上报器基本一致。

2.2 使用范例

以下代码为最简化示例,其中CommunicatorConfig里的配置采用默认值,communicator通过CommunicatorConfig配置生成后,直接指定远程服务对象的具体服务对象名、IP和端口生成一个远程服务代理对象。

Tars Java代码使用范例// 先初始化基本Tars配置CommunicatorConfig cfg = new CommunicatorConfig();// 通过上述的CommunicatorConfig配置生成一个Communicator对象。Communicator communicator = CommunicatorFactory.getInstance().getCommunicator(cfg);// 指定Tars远程服务的服务对象名、IP和端口生成一个远程服务代理对象。

// 先初始化基本Tars配置 CommunicatorConfig cfg = new CommunicatorConfig(); // 通过上述的CommunicatorConfig配置生成一个Communicator对象。 Communicator communicator = CommunicatorFactory.getInstance().getCommunicator(cfg); // 指定Tars远程服务的服务对象名、IP和端口生成一个远程服务代理对象。 HelloPrx proxy = communicator.stringToProxy(HelloPrx.class, "TestApp.HelloServer.HelloObj@tcp -h 127.0.0.1 -p 18601 -t 60000"); //同步调用,阻塞直到远程服务对象的方法返回结果 String ret = proxy.hello(3000, "Hello World"); System.out.println(ret); //异步调用,不关注异步调用最终的情况 proxy.async_hello(null, 3000, "Hello World"); //异步调用,注册一个实现TarsAbstractCallback接口的回执处理对象,该实现类分别处理调用成功,调用超时和调用异常的情况。 proxy.async_hello(new HelloPrxCallback() { @Override public void callback_expired() { //超时事件处理 } @Override public void callback_exception(Throwable ex) { //异常事件处理 } @Override public void callback_hello(String ret) { //调用成功事件处理 Main.logger.info("invoke async method successfully {}", ret); } }, 1000, "Hello World"); 在上述例子中,演示了常见的两种调用方式,分别为同步调用和异步调用。其中异步调用,如果调用方想捕捉异步调用的最终结果,可以注册一个实现TarsAbstractCallback接口的实现类,对tars调用的异常,超时和成功事件进行处理。

2.3 代理生成

Tars Java的客户端桩模块的远程代理对象是采用JDK原生Proxy方法。如下文的源码所示,ObjectProxy实现了java.lang.reflect.InvocationHandler的接口方法,该接口是JDK自带的代理接口。

代理实现

public final class ObjectProxy<T> implements ServantProxy, InvocationHandler { public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { String methodName = method.getName(); Class<?>[] parameterTypes = method.getParameterTypes(); InvokeContext context = this.protocolInvoker.createContext(proxy, method, args); try { if ("toString".equals(methodName) && parameterTypes.length == 0) { return this.toString(); } else if //***** 省略代码 ***** } else { // 在负载均衡器选取一个远程调用类,进行应用层协议的封装,最后调用TCP传输层进行发送。 Invoker invoker = this.loadBalancer.select(context); return invoker.invoke(context); } } catch (Throwable var8) { // ***** 省略代码 ***** } } } 当然生成上述远程服务代理类,涉及到辅助类,Tars Java采用ServantProxyFactory来生成上述的ObjectProxy,并存储ObjectProxy对象到Map结构,便于调用方二次使用时直接复用已存在的远程服务代理对象。

具体相关逻辑如源码所示,ObjectProxyFactory是生成ObjectProxy的辅助工厂类,和ServantProxyFactory不同,其本身不缓存生成的代理对象。

class ServantProxyFactory { private final ConcurrentHashMap<String, Object> cache = new ConcurrentHashMap(); // ***** 省略代码 ***** public <T> Object getServantProxy(Class<T> clazz, String objName, ServantProxyConfig servantProxyConfig, LoadBalance loadBalance, ProtocolInvoker<T> protocolInvoker) { Object proxy = this.cache.get(objName); if (proxy == null) { this.lock.lock(); // 加锁,保证只生成一个远程服务代理对象。 try { proxy = this.cache.get(objName); if (proxy == null) { // 创建实现JDK的java.lang.reflect.InvocationHandler接口的对象 ObjectProxy<T> objectProxy = this.communicator.getObjectProxyFactory().getObjectProxy(clazz, objName, servantProxyConfig, loadBalance, protocolInvoker); // 使用JDK的java.lang.reflect.Proxy来生成实际的代理对象 this.cache.putIfAbsent(objName, this.createProxy(clazz, objectProxy)); proxy = this.cache.get(objName); } } finally { this.lock.unlock(); } } return proxy; } /** 使用JDK自带的Proxy.newProxyInstance生成代理对象 */ private <T> Object createProxy(Class<T> clazz, ObjectProxy<T> objectProxy) { return Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), new Class[]{clazz, ServantProxy.class}, objectProxy); } // ***** 省略代码 ***** } 从以上的源码中,可以看到createProxy使用了JDK的Proxy.newProxyInstance方法来生成远程服务代理对象。

2.4 远程服务寻址方法

作为一个RPC远程框架,在分布式系统中,调用远程服务,涉及到如何路由的问题,也就是如何从多个远程服务节点中选择一个服务节点进行调用,当然Tars Java支持直连特定节点的方式调用远程服务,如上文的2.2 使用范例所介绍。

如图下图所示,ClientA某个时刻的一次调用使用了Service3节点进行远程服务调用,而ClientB某个时刻的一次调用采用Service2节点。Tars Java提供多种负载均衡算法实现类,其中有采用RR轮训算法的RoundRobinLoadBalance,一致性哈希算法的ConsistentHashLoadBalance和普通哈希算法的HashLoadBalance。

(客户端按特定路由规则调用远程服务) 如下述源码所示,如果要自定义负载均衡器来定义远程调用的路由规则,那么需要实现com.qq.tars.rpc.common.LoadBalance接口,其中LoadBalance.select()方法负责按照路由规则,选取对应的Invoker对象,然后进行远程调用,具体逻辑见源码代理实现。由于远程服务节点可能发生变更,比如上下线远程服务节点,需要刷新本地负载均衡器的路由信息,那么此信息更新的逻辑在LoadBalance.refresh()方法里实现。

负载均衡接口

public interface LoadBalance<T> { /** 根据负载均衡策略,挑选invoker */ Invoker<T> select(InvokeContext invokeContext) throws NoInvokerException; /** 通知invoker列表的更新 */ void refresh(Collection<Invoker<T>> invokers); }

2.5 网络模型

Tars Java的IO模式采用的JDK的NIO的Selector模式。这里以TCP协议来描述网络处理,如下述源码所示,Reactor是一个线程,其中的run()方法中,调用了selector.select()方法,意思是如果除非此时网络产生一个事件,否则将一直线程阻塞下去。

假如此时出现一个网络事件,那么此时线程将会被唤醒,执行后续代码,其中一个代码是dispatcheEvent(key),也就是将进行事件的分发。

其中将根据对应条件,调用acceptor.handleConnectEvent(key)方法来处理客户端连接成功事件,或acceptor.handleAcceptEvent(key)方法来处理服务器接受连接成功事件,或调用acceptor.handleReadEvent(key)方法从Socket里读取数据,或acceptor.handleWriteEvent(key)方法来写数据到Socket 。

Reactor事件处理

public final class Reactor extends Thread { protected volatile Selector selector = null; private Acceptor acceptor = null; //***** 省略代码 ***** public void run() { try { while (!Thread.interrupted()) { // 阻塞直到有网络事件发生。 selector.select(); //***** 省略代码 ***** while (iter.hasNext()) { SelectionKey key = iter.next(); iter.remove(); if (!key.isValid()) continue; try { //***** 省略代码 ***** // 分发传输层协议TCP或UDP网络事件 dispatchEvent(key); //***** 省略代码 ***** } } //***** 省略代码 ***** } //***** 省略代码 ***** private void dispatchEvent(final SelectionKey key) throws IOException { if (key.isConnectable()) { acceptor.handleConnectEvent(key); } else if (key.isAcceptable()) { acceptor.handleAcceptEvent(key); } else if (key.isReadable()) { acceptor.handleReadEvent(key); } else if (key.isValid() && key.isWritable()) { acceptor.handleWriteEvent(key); } } } 网络处理采用Reactor事件驱动模式,Tars定义一个Reactor对象对应一个Selector对象,针对每个远程服务(整体服务集群,非单个节点程序)默认创建2个Reactor对象进行处理,通过修改com.qq.tars.net.client.selectorPoolSize这个JVM启动参数值来决定一个远程服务具体创建几个Reactor对象。

(Tars-Java的网络事件处理模型) 上图中的处理读IO事件(Read Event)实现和写IO事件(Write Event)的线程池是在Communicator初始化的时候配置的。具体逻辑如源码所示,其中线程池参数配置由CommunicatorConfig的corePoolSize, maxPoolSize, keepAliveTime等参数决定。

读写事件线程池初始化

private void initCommunicator(CommunicatorConfig config) throws CommunicatorConfigException { //***** 省略代码 ***** this.threadPoolExecutor = ClientPoolManager.getClientThreadPoolExecutor(config); //***** 省略代码 ***** } ​ public class ClientPoolManager { public static ThreadPoolExecutor getClientThreadPoolExecutor(CommunicatorConfig communicatorConfig) { //***** 省略代码 ***** clientThreadPoolMap.put(communicatorConfig, createThreadPool(communicatorConfig)); //***** 省略代码 ***** return clientPoolExecutor; } private static ThreadPoolExecutor createThreadPool(CommunicatorConfig communicatorConfig) { int corePoolSize = communicatorConfig.getCorePoolSize(); int maxPoolSize = communicatorConfig.getMaxPoolSize(); int keepAliveTime = communicatorConfig.getKeepAliveTime(); int queueSize = communicatorConfig.getQueueSize(); TaskQueue taskqueue = new TaskQueue(queueSize); ​ String namePrefix = "tars-client-executor-"; TaskThreadPoolExecutor executor = new TaskThreadPoolExecutor(corePoolSize, maxPoolSize, keepAliveTime, TimeUnit.SECONDS, taskqueue, new TaskThreadFactory(namePrefix)); taskqueue.setParent(executor); return executor; } }

2.6 远程调用交互模型

调用代理类的方法,那么会进入实现InvocationHandler接口的ObjectProxy中的invoke方法。

下图描述了远程服务调用的流程情况。这里着重讲几个点,一个是如何写数据到网络IO。第二个是Tars Java通过什么方式进行同步或者异步调用,底层采用了什么技术。

(远程调用流程)

2.6.1 写 IO 流程

如图(底层代码写IO过程)所示,ServantClient将调用底层网络写操作,在invokeWithSync方法中,取得ServantClient自身成员变量TCPSession,调用TCPSession.write()方法,如图(底层代码写IO过程)和以下源码( 读写事件线程池初始化)所示,先获取Encode进行请求内容编码成IoBuffer对象,最后将IoBuffer的java.nio.ByteBuffer内容放入TCPSession的queue成员变量中,然后调用key.selector().wakeup(),唤醒Reactor中run()方法中的Selector.select(),执行后续的写操作。

(底层代码写IO过程)

具体Reactor逻辑见上文2.5 网络模型内容,如果Reactor检查条件发现可以写IO的话也就是key.isWritable()为true,那么最终会循环从TCPSession.queue中取出ByteBuffer对象,调用SocketChannel.write(byteBuffer)执行实际的写网络Socket操作,代码逻辑见源码中的doWrite()方法。

读写事件线程池初始化

public class TCPSession extends Session { public void write(Request request) throws IOException { try { IoBuffer buffer = selectorManager.getProtocolFactory().getEncoder().encodeRequest(request, this); write(buffer); //***** 省略代码 ***** } protected void write(IoBuffer buffer) throws IOException { //***** 省略代码 ***** if (!this.queue.offer(buffer.buf())) { throw new IOException("The session queue is full. [ queue size:" + queue.size() + " ]"); } if (key != null) { key.interestOps(key.interestOps() | SelectionKey.OP_WRITE); key.selector().wakeup(); } } protected synchronized int doWrite() throws IOException { int writeBytes = 0; while (true) { ByteBuffer wBuf = queue.peek(); //***** 省略代码 ***** int bytesWritten = ((SocketChannel) channel).write(wBuf); //***** 省略代码 ***** return writeBytes; } }

2.6.2 同步和异步调用的底层技术实现

对于同步方法调用,如图(远程调用流程)和源码(ServantClient的同步调用)所示,ServantClient调用底层网络写操作,在invokeWithSync方法中创建一个Ticket对象,Ticket顾名思义就是票的意思,这张票唯一标识本次网络调用情况。

ServantClient的同步调用

public class ServantClient { public <T extends ServantResponse> T invokeWithSync(ServantRequest request) throws IOException { //***** 省略代码 ***** ticket = TicketManager.createTicket(request, session, this.syncTimeout); Session current = session; current.write(request); if (!ticket.await(this.syncTimeout, TimeUnit.MILLISECONDS)) { //***** 省略代码 ***** response = ticket.response(); //***** 省略代码 ***** return response; //***** 省略代码 ***** return response; } } 如代码所示,在执行完session.write()操作后,紧接着执行ticket.await()方法,该方法线程等待直到远程服务回复返回结果到客户端,ticket.await()被唤醒后,将执行后续操作,最终invokeWithSync方法返回response对象。其中Ticket的等待唤醒功能内部采用java.util.concurrent.CountDownLatch来实现。

对于异步方法调用,将会执行ServantClient.invokeWithAsync方法,也会创建一个Ticket,并且执行Session.write()操作,虽然不会调用ticket.await(),但是在Reactor接收到远程回复时,首先会先解析Tars协议头得到Response对象,然后将Response对象放入如图(Tars-Java的网络事件处理模型)所示的IO读写线程池中进行进一步处理,如下述源码(异步回调事件处理)所示,最终会调用WorkThread.run()方法,在run()方法里执行ticket.notifyResponse(resp),该方法里面会执行类似上述代码2.1中的实现TarsAbstractCallback接口的调用成功回调的方法。

异步回调事件处理

public final class WorkThread implements Runnable { public void run() { try { //***** 省略代码 ***** Ticket<Response> ticket = TicketManager.getTicket(resp.getTicketNumber()); //***** 省略代码 ***** ticket.notifyResponse(resp); ticket.countDown(); TicketManager.removeTicket(ticket.getTicketNumber()); } //***** 省略代码 ***** } } 如下述源码所示,TicketManager会有一个定时任务轮训检查所有的调用是否超时,如果(currentTime - t.startTime) > t.timeout条件成立,那么会调用t.expired()告知回调对象,本次调用超时。

调用超时事件处理

public class TicketManager { //***** 省略代码 ***** static { executor.scheduleAtFixedRate(new Runnable() { long currentTime = -1; public void run() { Collection<Ticket<?>> values = tickets.values(); currentTime = System.currentTimeMillis(); for (Ticket<?> t : values) { if ((currentTime - t.startTime) > t.timeout) { removeTicket(t.getTicketNumber()); t.expired(); } } } }, 500, 500, TimeUnit.MILLISECONDS); } }

三、总结

代码的调用一般都是层层递归调用,代码的调用深度和广度都很大,通过调试代码的方式一步步学习源码的方式,更加容易理解源码的含义和设计理念。

Tars与其他RPC框架,并没有什么本质区别,通过类比其他框架的设计理念,可以更加深入理解Tars Java设计理念。

四、参考文献

1.Remote procedure call

2.Tars Java源码Github仓库

3.RPC框架简介与原理

作者:vivo 互联网服务器团队-Ke Shengkai

有关Tars-Java客户端源码分析的更多相关文章

  1. java - 等价于 Java 中的 Ruby Hash - 2

    我真的很习惯使用Ruby编写以下代码:my_hash={}my_hash['test']=1Java中对应的数据结构是什么? 最佳答案 HashMapmap=newHashMap();map.put("test",1);我假设? 关于java-等价于Java中的RubyHash,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.com/questions/22737685/

  2. java - 从 JRuby 调用 Java 类的问题 - 2

    我正在尝试使用boilerpipe来自JRuby。我看过guide从JRuby调用Java,并成功地将它与另一个Java包一起使用,但无法弄清楚为什么同样的东西不能用于boilerpipe。我正在尝试基本上从JRuby中执行与此Java等效的操作:URLurl=newURL("http://www.example.com/some-location/index.html");Stringtext=ArticleExtractor.INSTANCE.getText(url);在JRuby中试过这个:require'java'url=java.net.URL.new("http://www

  3. java - 我的模型类或其他类中应该有逻辑吗 - 2

    我只想对我一直在思考的这个问题有其他意见,例如我有classuser_controller和classuserclassUserattr_accessor:name,:usernameendclassUserController//dosomethingaboutanythingaboutusersend问题是我的User类中是否应该有逻辑user=User.newuser.do_something(user1)oritshouldbeuser_controller=UserController.newuser_controller.do_something(user1,user2)我

  4. java - 什么相当于 ruby​​ 的 rack 或 python 的 Java wsgi? - 2

    什么是ruby​​的rack或python的Java的wsgi?还有一个路由库。 最佳答案 来自Python标准PEP333:Bycontrast,althoughJavahasjustasmanywebapplicationframeworksavailable,Java's"servlet"APImakesitpossibleforapplicationswrittenwithanyJavawebapplicationframeworktoruninanywebserverthatsupportstheservletAPI.ht

  5. UE4 源码阅读:从引擎启动到Receive Begin Play - 2

    一、引擎主循环UE版本:4.27一、引擎主循环的位置:Launch.cpp:GuardedMain函数二、、GuardedMain函数执行逻辑:1、EnginePreInit:加载大多数模块int32ErrorLevel=EnginePreInit(CmdLine);PreInit模块加载顺序:模块加载过程:(1)注册模块中定义的UObject,同时为每个类构造一个类默认对象(CDO,记录类的默认状态,作为模板用于子类实例创建)(2)调用模块的StartUpModule方法2、FEngineLoop::Init()1、检查Engine的配置文件找出使用了哪一个GameEngine类(UGame

  6. Observability:从零开始创建 Java 微服务并监控它 (二) - 2

    这篇文章是继上一篇文章“Observability:从零开始创建Java微服务并监控它(一)”的续篇。在上一篇文章中,我们讲述了如何创建一个Javaweb应用,并使用Filebeat来收集应用所生成的日志。在今天的文章中,我来详述如何收集应用的指标,使用APM来监控应用并监督web服务的在线情况。源码可以在地址 https://github.com/liu-xiao-guo/java_observability 进行下载。摄入指标指标被视为可以随时更改的时间点值。当前请求的数量可以改变任何毫秒。你可能有1000个请求的峰值,然后一切都回到一个请求。这也意味着这些指标可能不准确,你还想提取最小/

  7. 【Java 面试合集】HashMap中为什么引入红黑树,而不是AVL树呢 - 2

    HashMap中为什么引入红黑树,而不是AVL树呢1.概述开始学习这个知识点之前我们需要知道,在JDK1.8以及之前,针对HashMap有什么不同。JDK1.7的时候,HashMap的底层实现是数组+链表JDK1.8的时候,HashMap的底层实现是数组+链表+红黑树我们要思考一个问题,为什么要从链表转为红黑树呢。首先先让我们了解下链表有什么不好???2.链表上述的截图其实就是链表的结构,我们来看下链表的增删改查的时间复杂度增:因为链表不是线性结构,所以每次添加的时候,只需要移动一个节点,所以可以理解为复杂度是N(1)删:算法时间复杂度跟增保持一致查:既然是非线性结构,所以查询某一个节点的时候

  8. 【Java入门】使用Java实现文件夹的遍历 - 2

    遍历文件夹我们通常是使用递归进行操作,这种方式比较简单,也比较容易理解。本文为大家介绍另一种不使用递归的方式,由于没有使用递归,只用到了循环和集合,所以效率更高一些!一、使用递归遍历文件夹整体思路1、使用File封装初始目录,2、打印这个目录3、获取这个目录下所有的子文件和子目录的数组。4、遍历这个数组,取出每个File对象4-1、如果File是否是一个文件,打印4-2、否则就是一个目录,递归调用代码实现publicclassSearchFile{publicstaticvoidmain(String[]args){//初始目录Filedir=newFile("d:/Dev");Datebeg

  9. java - 为什么 ruby​​ modulo 与 java/other lang 不同? - 2

    我基本上来自Java背景并且努力理解Ruby中的模运算。(5%3)(-5%3)(5%-3)(-5%-3)Java中的上述操作产生,2个-22个-2但在Ruby中,相同的表达式会产生21个-1-2.Ruby在逻辑上有多擅长这个?模块操作在Ruby中是如何实现的?如果将同一个操作定义为一个web服务,两个服务如何匹配逻辑。 最佳答案 在Java中,模运算的结果与被除数的符号相同。在Ruby中,它与除数的符号相同。remainder()在Ruby中与被除数的符号相同。您可能还想引用modulooperation.

  10. java - Ruby 相当于 Java 的 Collections.unmodifiableList 和 Collections.unmodifiableMap - 2

    Java的Collections.unmodifiableList和Collections.unmodifiableMap在Ruby标准API中是否有等价物? 最佳答案 使用freeze应用程序接口(interface):Preventsfurthermodificationstoobj.ARuntimeErrorwillberaisedifmodificationisattempted.Thereisnowaytounfreezeafrozenobject.SeealsoObject#frozen?.Thismethodretur

随机推荐