dubbo源码学习

做web开发也有小几个月了,基本就是做做业务上的开发,有空的时候看一些源码,如redis,lombok等,前几天我司前端准备搞一个基于node的dubbo框架,作为对dubbo和node都有一点点了解的人,被他们拉去一起调试问题,调试过程中发生了比较多的问题,由于没有阅读过dubbo的源码,导致一时不知道如何回答,这周末趁着有时间,走读了一遍,记录下来,以防遗忘。

感谢

还是按照我的个人习惯,感谢放在文章的开头。

远程服务的发布与引用

dubbo源码解析-服务暴露原理

Dubbo中服务消费者和服务提供者之间的请求和响应过程

其中强烈推荐第二个作者,是一个系列,看完之后收获很大,真心感谢!

服务注册

dubbo主要分成2个部分:provider和consumer,服务的提供者和消费者,我们先从提供者开始说起。

服务的注册首先先看ServiceConfig这个类,调用其export方法,而在export方法中,通过doExport->doExportUrls->doExportUrlsFor1Protocol的调用链,最终调用的是doExportUrlsFor1Protocol方法。

这个方法很长,但是其中有2个部分需要注意,就是会判断这个服务是不是本地服务从而判断是否调用exportLocal。本地服务就相当于是直连,dubbo通过这种方式来提升效率。exportLocal的具体逻辑大家可以去看感谢中的第二个作者的源码解析,比较清楚。

如果不是直连的,则会走下面这段代码:

1
2
3
Invoker<?> invoker = proxyFactory.getInvoker(this.ref, this.interfaceClass, registryURL.addParameterAndEncoded("export", url.toFullString()));
Exporter<?> exporter = protocol.export(invoker);
this.exporters.add(exporter);

先获取到一个invoker,然后通过protocol将invoker转化成一个export,其中interfaceClass就是你注册的那个类的类名。

1
2
3
4
5
6
7
8
@SPI("javassist")
public interface ProxyFactory {
@Adaptive({"proxy"})
<T> T getProxy(Invoker<T> var1) throws RpcException;

@Adaptive({"proxy"})
<T> Invoker<T> getInvoker(T var1, Class<T> var2, URL var3) throws RpcException;
}

可以看到dubbo中用了SPI,这种模式在dubbo中随处可见,对于做Android的我算是比较新鲜了,就像当初发现ServiceLoader一样。下面我们来看JavassistProxyFactory。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class JavassistProxyFactory extends AbstractProxyFactory {
public JavassistProxyFactory() {
}

public <T> T getProxy(Invoker<T> invoker, Class<?>[] interfaces) {
return Proxy.getProxy(interfaces).newInstance(new InvokerInvocationHandler(invoker));
}

public <T> Invoker<T> getInvoker(T proxy, Class<T> type, URL url) {
final Wrapper wrapper = Wrapper.getWrapper(proxy.getClass().getName().indexOf(36) < 0?proxy.getClass():type);
return new AbstractProxyInvoker<T>(proxy, type, url) {
protected Object doInvoke(T proxy, String methodName, Class<?>[] parameterTypes, Object[] arguments) throws Throwable {
return wrapper.invokeMethod(proxy, methodName, parameterTypes, arguments);
}
};
}
}

getInvoker方法其实就是通过动态代理的方式去创建一个invoker,动态代理同样在dubbo中应用广泛。

回到上面,创建完成了这个invoker之后,通过protocol将其转换成exporter。

1
2
3
4
5
6
7
8
9
10
11
12
@SPI("dubbo")
public interface Protocol {
int getDefaultPort();

@Adaptive
<T> Exporter<T> export(Invoker<T> var1) throws RpcException;

@Adaptive
<T> Invoker<T> refer(Class<T> var1, URL var2) throws RpcException;

void destroy();
}

protocol也是一样通过SPI的方式去处理的,这里我们去看RegistryProtocol。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public <T> Exporter<T> export(Invoker<T> originInvoker) throws RpcException {
final RegistryProtocol.ExporterChangeableWrapper<T> exporter = this.doLocalExport(originInvoker);
final Registry registry = this.getRegistry(originInvoker);
final URL registedProviderUrl = this.getRegistedProviderUrl(originInvoker);
registry.register(registedProviderUrl);
final URL overrideSubscribeUrl = this.getSubscribedOverrideUrl(registedProviderUrl);
final RegistryProtocol.OverrideListener overrideSubscribeListener = new RegistryProtocol.OverrideListener(overrideSubscribeUrl);
this.overrideListeners.put(overrideSubscribeUrl, overrideSubscribeListener);
registry.subscribe(overrideSubscribeUrl, overrideSubscribeListener);
return new Exporter<T>() {
public Invoker<T> getInvoker() {
return exporter.getInvoker();
}

public void unexport() {
try {
exporter.unexport();
} catch (Throwable var4) {
RegistryProtocol.logger.warn(var4.getMessage(), var4);
}

try {
registry.unregister(registedProviderUrl);
} catch (Throwable var3) {
RegistryProtocol.logger.warn(var3.getMessage(), var3);
}

try {
RegistryProtocol.this.overrideListeners.remove(overrideSubscribeUrl);
registry.unsubscribe(overrideSubscribeUrl, overrideSubscribeListener);
} catch (Throwable var2) {
RegistryProtocol.logger.warn(var2.getMessage(), var2);
}

}
};
}

代码很长,但是我们就看其中关键的一句:

1
final Registry registry = this.getRegistry(originInvoker);
1
2
3
4
5
6
7
8
9
private Registry getRegistry(Invoker<?> originInvoker) {
URL registryUrl = originInvoker.getUrl();
if("registry".equals(registryUrl.getProtocol())) {
String protocol = registryUrl.getParameter("registry", "dubbo");
registryUrl = registryUrl.setProtocol(protocol).removeParameter("registry");
}

return this.registryFactory.getRegistry(registryUrl);
}

我们来看AbstractRegistryFactory的getRegistry方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public Registry getRegistry(URL url) {
url = url.setPath(RegistryService.class.getName()).addParameter("interface", RegistryService.class.getName()).removeParameters(new String[]{"export", "refer"});
String key = url.toServiceString();
LOCK.lock();

Registry var4;
try {
Registry registry = (Registry)REGISTRIES.get(key);
if(registry == null) {
registry = this.createRegistry(url);
if(registry == null) {
throw new IllegalStateException("Can not create registry " + url);
}

REGISTRIES.put(key, registry);
var4 = registry;
return var4;
}

var4 = registry;
} finally {
LOCK.unlock();
}

return var4;
}

核心逻辑在createRegistry中,而这个方法是abstract的,这里由于我们要结合zk,所以来看ZookeeperRegistryFactory。

1
2
3
public Registry createRegistry(URL url) {
return new ZookeeperRegistry(url, this.zookeeperTransporter);
}

创建了一个ZookeeperRegistry。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public ZookeeperRegistry(URL url, ZookeeperTransporter zookeeperTransporter) {
super(url);
if(url.isAnyHost()) {
throw new IllegalStateException("registry address == null");
} else {
String group = url.getParameter("group", "dubbo");
if(!group.startsWith("/")) {
group = "/" + group;
}

this.root = group;
this.zkClient = zookeeperTransporter.connect(url);
this.zkClient.addStateListener(new StateListener() {
public void stateChanged(int state) {
if(state == 2) {
try {
ZookeeperRegistry.this.recover();
} catch (Exception var3) {
ZookeeperRegistry.logger.error(var3.getMessage(), var3);
}
}

}
});
CommandServer commandServer = CommandServer.getInstance();
this.zkHandler = new ZookeeperHandler(this);
commandServer.registerHandler("dubbo", this.zkHandler);
}
}

构造函数中最重要的一步就是创建zkClient。

1
this.zkClient = zookeeperTransporter.connect(url);

我们知道zk是dubbo官方推荐的一个注册中心,从代码中我们也可以看出dubbo是如何使用zk的。

其中有一个小知识点,我们来看ZookeeperRegistry的父类AbstractRegistry的构造函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public AbstractRegistry(URL url) {
this.setUrl(url);
this.syncSaveFile = url.getParameter("save.file", false);
String filename = url.getParameter("file", System.getProperty("user.home") + "/.dubbo/dubbo-registry-" + url.getHost() + ".cache");
File file = null;
if(ConfigUtils.isNotEmpty(filename)) {
file = new File(filename);
if(!file.exists() && file.getParentFile() != null && !file.getParentFile().exists() && !file.getParentFile().mkdirs()) {
throw new IllegalArgumentException("Invalid registry store file " + file + ", cause: Failed to create directory " + file.getParentFile() + "!");
}
}

this.file = file;
this.loadProperties();
this.notify(url.getBackupUrls());
}

其中有一个步骤是loadProperties。这一步的操作是去做注册的缓存,如果注册中心挂了,则可以走本地缓存,保证服务还是调用的通的。

到此为止,我们的服务就已经注册到了zk上了,注册完成之后,服务真正的监听是在DubboProtocol中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public <T> Exporter<T> export(Invoker<T> invoker) throws RpcException {
URL url = invoker.getUrl();
String key = serviceKey(url);
DubboExporter<T> exporter = new DubboExporter(invoker, key, this.exporterMap);
this.exporterMap.put(key, exporter);
Boolean isStubSupportEvent = Boolean.valueOf(url.getParameter("dubbo.stub.event", false));
Boolean isCallbackservice = Boolean.valueOf(url.getParameter("is_callback_service", false));
if(isStubSupportEvent.booleanValue() && !isCallbackservice.booleanValue()) {
String stubServiceMethods = url.getParameter("dubbo.stub.event.methods");
if(stubServiceMethods != null && stubServiceMethods.length() != 0) {
this.stubServiceMethodsMap.put(url.getServiceKey(), stubServiceMethods);
} else if(this.logger.isWarnEnabled()) {
this.logger.warn(new IllegalStateException("consumer [" + url.getParameter("interface") + "], has set stubproxy support event ,but no stub methods founded."));
}
}

this.openServer(url);
return exporter;
}

其中最重要的就是openServer方法,在openServer中,dubbo通过netty去创建了一个server,用来监听调用方的tcp请求。

至此,dubbo的服务启动就大致浏览完毕了,其中有几个点需要记住:

  1. dubbo存在本地直连,通过InjvmExport来实现,和DubboExport做了区分,用以提高效率。
  2. 服务启动的过程就是创建invoker,并且通过protocol将其转换转换成export的过程。
  3. protocol中采用了zk做为注册中心,netty作为网络层服务。
  4. protocol中存在缓存,如果注册中心挂了,服务还是可以走通。

服务调用

同样的,服务调用通过ReferenceConfig完成,我们来看ReferenceConfig的createProxy方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
private T createProxy(Map<String, String> map) {
new URL("temp", "localhost", 0, map);
boolean isJvmRefer;
if(this.isInjvm() == null) {
if(this.url != null && this.url.length() > 0) {
isJvmRefer = false;
} else {
isJvmRefer = false;
}
} else {
isJvmRefer = this.isInjvm().booleanValue();
}

if(isJvmRefer) {
URL url = (new URL("injvm", "127.0.0.1", 0, this.interfaceClass.getName())).addParameters(map);
this.invoker = refprotocol.refer(this.interfaceClass, url);
if(logger.isInfoEnabled()) {
logger.info("Using injvm service " + this.interfaceClass.getName());
}
} else {
URL u;
URL url;
if(this.url != null && this.url.length() > 0) {
String[] us = Constants.SEMICOLON_SPLIT_PATTERN.split(this.url);
if(us != null && us.length > 0) {
String[] arr$ = us;
int len$ = us.length;

for(int i$ = 0; i$ < len$; ++i$) {
String u = arr$[i$];
URL url = URL.valueOf(u);
if(url.getPath() == null || url.getPath().length() == 0) {
url = url.setPath(this.interfaceName);
}

if("registry".equals(url.getProtocol())) {
this.urls.add(url.addParameterAndEncoded("refer", StringUtils.toQueryString(map)));
} else {
this.urls.add(ClusterUtils.mergeUrl(url, map));
}
}
}
} else {
List<URL> us = this.loadRegistries(false);
if(us != null && us.size() > 0) {
for(Iterator i$ = us.iterator(); i$.hasNext(); this.urls.add(u.addParameterAndEncoded("refer", StringUtils.toQueryString(map)))) {
u = (URL)i$.next();
url = this.loadMonitor(u);
if(url != null) {
map.put("monitor", URL.encode(url.toFullString()));
}
}
}

if(this.urls == null || this.urls.size() == 0) {
throw new IllegalStateException("No such any registry to reference " + this.interfaceName + " on the consumer " + NetUtils.getLocalHost() + " use dubbo version " + Version.getVersion() + ", please config <dubbo:registry address=\"...\" /> to your spring config.");
}
}

if(this.urls.size() == 1) {
this.invoker = refprotocol.refer(this.interfaceClass, (URL)this.urls.get(0));
} else {
List<Invoker<?>> invokers = new ArrayList();
URL registryURL = null;
Iterator i$ = this.urls.iterator();

while(i$.hasNext()) {
url = (URL)i$.next();
invokers.add(refprotocol.refer(this.interfaceClass, url));
if("registry".equals(url.getProtocol())) {
registryURL = url;
}
}

if(registryURL != null) {
u = registryURL.addParameter("cluster", "available");
this.invoker = cluster.join(new StaticDirectory(u, invokers));
} else {
this.invoker = cluster.join(new StaticDirectory(invokers));
}
}
}

Boolean c = this.check;
if(c == null && this.consumer != null) {
c = this.consumer.isCheck();
}

if(c == null) {
c = Boolean.valueOf(true);
}

if(c.booleanValue() && !this.invoker.isAvailable()) {
throw new IllegalStateException("Failed to check the status of the service " + this.interfaceName + ". No provider available for the service " + (this.group == null?"":this.group + "/") + this.interfaceName + (this.version == null?"":":" + this.version) + " from the url " + this.invoker.getUrl() + " to the consumer " + NetUtils.getLocalHost() + " use dubbo version " + Version.getVersion());
} else {
if(logger.isInfoEnabled()) {
logger.info("Refer dubbo service " + this.interfaceClass.getName() + " from url " + this.invoker.getUrl());
}

return proxyFactory.getProxy(this.invoker);
}
}

代码非常的长,我们重点关注以下2点(Injvm的直连就不说了):

1
2
3
this.invoker = refprotocol.refer(this.interfaceClass, (URL)this.urls.get(0));
......
return proxyFactory.getProxy(this.invoker);

和服务注册一样,也是通过protol去获取invoker,然后通过ProxyFactory去获取代理,我们来看RegistryProtocol。

1
2
3
4
5
6
7
8
9
10
11
12
private <T> Invoker<T> doRefer(Cluster cluster, Registry registry, Class<T> type, URL url) {
RegistryDirectory<T> directory = new RegistryDirectory(type, url);
directory.setRegistry(registry);
directory.setProtocol(this.protocol);
URL subscribeUrl = new URL("consumer", NetUtils.getLocalHost(), 0, type.getName(), directory.getUrl().getParameters());
if(!"*".equals(url.getServiceInterface()) && url.getParameter("register", true)) {
registry.register(subscribeUrl.addParameters(new String[]{"category", "consumers", "check", String.valueOf(false)}));
}

directory.subscribe(subscribeUrl.addParameter("category", "providers,configurators,routers"));
return cluster.join(directory);
}

doRefer中创建了一个RegistryDirectory。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public RegistryDirectory(Class<T> serviceType, URL url) {
super(url);
if(serviceType == null) {
throw new IllegalArgumentException("service type is null.");
} else if(url.getServiceKey() != null && url.getServiceKey().length() != 0) {
this.serviceType = serviceType;
this.serviceKey = url.getServiceKey();
this.queryMap = StringUtils.parseQueryString(url.getParameterAndDecoded("refer"));
this.overrideDirectoryUrl = this.directoryUrl = url.setPath(url.getServiceInterface()).clearParameters().addParameters(this.queryMap).removeParameter("monitor");
String group = this.directoryUrl.getParameter("group", "");
this.multiGroup = group != null && ("*".equals(group) || group.contains(","));
String methods = (String)this.queryMap.get("methods");
this.serviceMethods = methods == null?null:Constants.COMMA_SPLIT_PATTERN.split(methods);
} else {
throw new IllegalArgumentException("registry serviceKey is null.");
}
}

这个RegistryDirectory可以看做对dubbo协议(也就是url)的一个解析,拿到query,method等参数。

返回去看,最后调用了cluster.join(directory),感觉和集群相关了,屌屌的。

1
2
3
4
5
@SPI("failfast")
public interface Cluster {
@Adaptive
<T> Invoker<T> join(Directory<T> var1) throws RpcException;
}

又见SPI,我们去看MockClusterWrapper。

1
2
3
4
5
6
7
8
9
10
11
public class MockClusterWrapper implements Cluster {
private Cluster cluster;

public MockClusterWrapper(Cluster cluster) {
this.cluster = cluster;
}

public <T> Invoker<T> join(Directory<T> directory) throws RpcException {
return new MockClusterInvoker(directory, this.cluster.join(directory));
}
}

创建了一个MockClusterInvoker。

回到最开始的地方:

1
2
3
this.invoker = refprotocol.refer(this.interfaceClass, (URL)this.urls.get(0));
......
return proxyFactory.getProxy(this.invoker);

拿到了invoker之后,调用了proxyFactory.getProxy(this.invoker)方法。

1
2
3
public <T> T getProxy(Invoker<T> invoker, Class<?>[] interfaces) {
return Proxy.getProxy(interfaces).newInstance(new InvokerInvocationHandler(invoker));
}

在InvokerInvocationHandler中,会调用invoker的invoke方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public Result invoke(Invocation invocation) throws RpcException {
Result result = null;
String value = this.directory.getUrl().getMethodParameter(invocation.getMethodName(), "mock", Boolean.FALSE.toString()).trim();
if(value.length() != 0 && !value.equalsIgnoreCase("false")) {
if(value.startsWith("force")) {
if(logger.isWarnEnabled()) {
logger.info("force-mock: " + invocation.getMethodName() + " force-mock enabled , url : " + this.directory.getUrl());
}

result = this.doMockInvoke(invocation, (RpcException)null);
} else {
try {
result = this.invoker.invoke(invocation);
} catch (RpcException var5) {
if(var5.isBiz()) {
throw var5;
}

if(logger.isWarnEnabled()) {
logger.info("fail-mock: " + invocation.getMethodName() + " fail-mock enabled , url : " + this.directory.getUrl(), var5);
}

result = this.doMockInvoke(invocation, var5);
}
}
} else {
result = this.invoker.invoke(invocation);
}

return result;
}

可以看到里面通过mock的方式处理了降级的逻辑,最终调用了AbstractClusterInvoker的invoke方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
public Result invoke(Invocation invocation) throws RpcException {
this.checkWheatherDestoried();
List<Invoker<T>> invokers = this.list(invocation);
LoadBalance loadbalance;
if(invokers != null && invokers.size() > 0) {
loadbalance = (LoadBalance)ExtensionLoader.getExtensionLoader(LoadBalance.class).getExtension(((Invoker)invokers.get(0)).getUrl().getMethodParameter(invocation.getMethodName(), "loadbalance", "random"));
} else {
loadbalance = (LoadBalance)ExtensionLoader.getExtensionLoader(LoadBalance.class).getExtension("random");
}

RpcUtils.attachInvocationIdIfAsync(this.getUrl(), invocation);
return this.doInvoke(invocation, invokers, loadbalance);
}

这里重点关注list方法。

1
2
3
4
protected List<Invoker<T>> list(Invocation invocation) throws RpcException {
List<Invoker<T>> invokers = this.directory.list(invocation);
return invokers;
}

通过directory去获取到了所有的invokers。其中通过SPI的方式最终调用的是RegistryDirectory。

RegistryDirectory我们前面说过,是对dubbo协议的一层解析,其中有一个notify方法方法,在注册中心有变动的时候主动更新其中的数据。

在通过RegistryDirectory拿到集群中所有的invokers之后,会通过router的方式去挑选出所有可用的invokers,并执行doInvoke方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public Result doInvoke(Invocation invocation, List<Invoker<T>> invokers, LoadBalance loadbalance) throws RpcException {
this.checkInvokers(invokers, invocation);
Invoker invoker = this.select(loadbalance, invocation, invokers, (List)null);

try {
return invoker.invoke(invocation);
} catch (Throwable var6) {
if(var6 instanceof RpcException && ((RpcException)var6).isBiz()) {
throw (RpcException)var6;
} else {
throw new RpcException(var6 instanceof RpcException?((RpcException)var6).getCode():0, "Failfast invoke providers " + invoker.getUrl() + " " + loadbalance.getClass().getSimpleName() + " select from all providers " + invokers + " for service " + this.getInterface().getName() + " method " + invocation.getMethodName() + " on consumer " + NetUtils.getLocalHost() + " use dubbo version " + Version.getVersion() + ", but no luck to perform the invocation. Last error is: " + var6.getMessage(), var6.getCause() != null?var6.getCause():var6);
}
}
}

其中经过负载均衡的一系列算法之后,获取到真正的invoker进行调用,而这个invoker就是DubboInvoker。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
protected Result doInvoke(Invocation invocation) throws Throwable {
RpcInvocation inv = (RpcInvocation)invocation;
String methodName = RpcUtils.getMethodName(invocation);
inv.setAttachment("path", this.getUrl().getPath());
inv.setAttachment("version", this.version);
ExchangeClient currentClient;
if(this.clients.length == 1) {
currentClient = this.clients[0];
} else {
currentClient = this.clients[this.index.getAndIncrement() % this.clients.length];
}

try {
boolean isAsync = RpcUtils.isAsync(this.getUrl(), invocation);
boolean isOneway = RpcUtils.isOneway(this.getUrl(), invocation);
int timeout = this.getUrl().getMethodParameter(methodName, "timeout", 1000);
if(isOneway) {
boolean isSent = this.getUrl().getMethodParameter(methodName, "sent", false);
currentClient.send(inv, isSent);
RpcContext.getContext().setFuture((Future)null);
return new RpcResult();
} else if(isAsync) {
ResponseFuture future = currentClient.request(inv, timeout);
RpcContext.getContext().setFuture(new FutureAdapter(future));
return new RpcResult();
} else {
RpcContext.getContext().setFuture((Future)null);
return (Result)currentClient.request(inv, timeout).get();
}
} catch (TimeoutException var9) {
throw new RpcException(2, "Invoke remote method timeout. method: " + invocation.getMethodName() + ", provider: " + this.getUrl() + ", cause: " + var9.getMessage(), var9);
} catch (RemotingException var10) {
throw new RpcException(1, "Failed to invoke remote method: " + invocation.getMethodName() + ", provider: " + this.getUrl() + ", cause: " + var10.getMessage(), var10);
}
}

可以看到会去判断这次调用是否是onWay,是否是Async,并且发送tpc请求。

至此,dubbo的服务调用就大致浏览完毕了,其中有几个点需要记住:

  1. 服务的调用是通过protocol去获取invoker,然后调用invoker的invoke去实现的。
  2. MockClusterInvoker会去处理降级和负载的逻辑,最终调用DubboInvoker。
  3. DubboInvoker中会处理oneWay和异步的逻辑,最终发送tcp请求调用。