Dubbo2.7源码分析-SPI的应用

前端之家收集整理的这篇文章主要介绍了Dubbo2.7源码分析-SPI的应用前端之家小编觉得挺不错的,现在分享给大家,也给大家做个参考。

SPI简介

SPI是Service Provider Interface的缩写,即服务提供接口(翻译出来好绕口,还是不翻译的好),实质上是接口,作用是对外提供服务。
SPI是Java的一种插件机制,可以不用修改代码实现新功能的扩展。
主要有如下几个步骤:

  1. 实现SPI接口
  2. 在项目的Meta-INF/services文件夹下,新建一个以SPI接口命名的文件文件里面配置上SPI接口的实现类
  3. 使用java.util.ServiceLoader加载。
    由于本篇文章主要讲解Dubbo是如何使用SPI的,如果想要具体了解Java的SPI,可以参考下面两篇文章

Dubbo SPI

回到正题,SPI在dubbo应用的地方很多,专业一点讲叫做微内核机制;
如下图:

Plug-In

我们拿其中一个标签进行讲解,我们在使用dubbo框架时,会配置<dubbo:protocol />标签,告诉dubbo服务的主机、端口、可接收的最大连接数、使用哪个协议,协议的传输控制器(netty,servlet,jetty等)、线程池类型大小等信息。dubbo协议默认使用的是netty网络传输框架,当然还可以使用mina、grizzly,只需要配置transporter、server、client为相应的值即可。那dubbo是如何根据不同的配置使用不同的网络传输框架的呢,当然是通过SPI啦。java spi有一个配置文件,那dubbo是否也有呢?在dubbo-rpc包下的dubbo-rpc-dubbo子包下,发现了一个配置文件

image.png

我们来看下配置文件内容

dubbo=org.apache.dubbo.rpc.protocol.dubbo.DubboProtocol

配置了一个键值对,key为dubbo,值为org.apache.dubbo.rpc.protocol.dubbo.DubboProtocol,在其它几个子包下,也有名称叫做org.apache.dubbo.rpc.Protocol的配置文件,说明Protocol插口有几个对应的插件

可以猜测一下,当<dubbo:protocol />仅仅配置了name="dubbo",port="20880"时,会加载哪一个协议插件呢,根据名称,可以猜测,加载的DubboProtocol插件。那dubbo是怎样做到的呢,我们来一探究竟。

Dubbo为使用SPI做的准备工作:

1. 三个注解

  • SPI:这个注解使用在接口上,标识接口是否是extension(扩展或插口),可以接收一个默认的extension名称
  • Adaptive: 这个注解可以使用在类或方法上,决定加载哪一个extension,值为字符串数组,数组中的字符串是key值,比如new String[]{"key1","key2"};先在URL中寻找key1的值,如果找到,则使用此值加载extension,如果key1没有,则寻找key2的值,如果key2也没有,则使用接口SPI注解的值,如果接口SPI注解,没有配置默认值,则将接口名按照首字母大写分成多个部分,然后以'.'分隔,例如org.apache.dubbo.xxx.YyyInvokerWrapper接口名会变成yyy.invoker.wrapper,然后以此名称做为key到URL寻找,如果仍没有找到,则抛出IllegalStateException异常;Adaptive注解用在类上,表示此类是它实现接口(插口)的自适应插件
  • Activate:这个注解可以使用在类或方法上,用以根据URL的key值判断当前extension是否生效,当一个extension有多个实现时,可以加载特定的extension实现类,例如extension实现类上有注解@Activate("cache,validation"),则当URL上出现"cache”或“validation" key时,当前extension才会生效

2. ExtensionLoader

顾名思义,ExtensionLoader用于加载extension,它的作用有三点:1.自动加载extension;2.自动包装(wrap) extension;3.创建自适应的(adaptive)extension;

旅途开始

先看下上篇文章中Provider端的配置文件

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <beans xmlns="http://www.springframework.org/schema/beans"
  3. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  4. xmlns:dubbo="http://dubbo.apache.org/schema/dubbo"
  5. 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">
  6. <!-- 提供方应用信息,用于计算依赖关系 -->
  7. <dubbo:application name="hello-world-app" />
  8. <!-- 使用multicast广播注册中心暴露服务地址 -->
  9. <dubbo:registry address="multicast://224.5.6.7:1234" />
  10. <!-- dubbo协议在20880端口暴露服务 -->
  11. <dubbo:protocol name="dubbo" port="20880" />
  12. <!-- 声明需要暴露的服务接口 -->
  13. <dubbo:service interface="com.alibaba.dubbo.demo.DemoService" ref="demoService" />
  14. <!-- 和本地bean一样实现服务 -->
  15. <bean id="demoService" class="com.alibaba.dubbo.demo.provider.DemoServiceImpl" />
  16. </beans>

还是先从ClassPathXmlApplicationContext加载spring配置文件说起,上回我们说到ClassPathXmlApplicationContext会使用XmlBeanDefinitionReader将xml文件解析成BeanDefiniton集合,当解析<dubbo:protocol />标签时,会将其解析成org.apache.dubbo.config.ProtocolConfig对象(为什么?请看上回分解最后,protocol key 实例化DubboBeanDefinitionParser时传入的参数),解析<dubbo:service />时,会将其解析成org.apache.dubbo.config.spring.ServiceBean对象。在解析xml时,会调用AbstractApplicationContext的refresh()方法

ServiceBean是ServiceConfig的子类,所以在创建ServiceBean对象的时候,会去先实例化父类,ServiceConfig中有一个static final成员变量protocol

  1. private static final Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension();

ExtensionLoader终于出场了,想要获取插件,得分两步走,第一步得到Protocol的插件加载对象extensionLoader,然后由这个加载对象获得对应的插件
先来看第一步:

  1. public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type) {
  2. //一些检查的代码,省略
  3. ExtensionLoader<T> loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
  4. if (loader == null) {
  5. EXTENSION_LOADERS.putIfAbsent(type,new ExtensionLoader<T>(type));
  6. loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
  7. }
  8. return loader;
  9. }

EXTENSION_LOADERS保存的是目前已经保存的插口的加载类,显示第一次加载的时候,Protocol还没有自己的插件加载类,那么需要实例化一个。实例化加载对象之后,用这个对象去加载插件

  1. public T getAdaptiveExtension() {
  2. //从已经缓存的自适应对象中获得,第一次调用时还没有创建自适应类,所以instance为null
  3. Object instance = cachedAdaptiveInstance.get();
  4. if (instance == null) {
  5. if (createAdaptiveInstanceError == null) {
  6. synchronized (cachedAdaptiveInstance) {
  7. instance = cachedAdaptiveInstance.get();
  8. if (instance == null) {
  9. try {
  10. //创建一个自适应类
  11. instance = createAdaptiveExtension();
  12. cachedAdaptiveInstance.set(instance);
  13. } catch (Throwable t) {
  14. createAdaptiveInstanceError = t;
  15. throw new IllegalStateException("fail to create adaptive instance: " + t.toString(),t);
  16. }
  17. }
  18. }
  19. } else {
  20. throw new IllegalStateException("fail to create adaptive instance: " + createAdaptiveInstanceError.toString(),createAdaptiveInstanceError);
  21. }
  22. }
  23. return (T) instance;
  24. }

主要关注 instance = createAdaptiveExtension();这句,createAdaptiveExtension()方法是什么样的呢?

  1. private T createAdaptiveExtension() {
  2. try {
  3. //得到自适应类并实现化,然后注入属性
  4. return injectExtension((T) getAdaptiveExtensionClass().newInstance());
  5. } catch (Exception e) {
  6. throw new IllegalStateException("Can not create adaptive extension " + type + ",cause: " + e.getMessage(),e);
  7. }
  8. }

getAdaptiveExtensionClass():

  1. private Class<?> getAdaptiveExtensionClass() {
  2. //1.获取所有实现Protocol插口的插件
  3. getExtensionClasses();
  4. //2.如果有自适应插件类,则返回
  5. if (cachedAdaptiveClass != null) {
  6. return cachedAdaptiveClass;
  7. }
  8. //3.如果没有,则创建插件
  9. return cachedAdaptiveClass = createAdaptiveExtensionClass();
  10. }

先来看上面的第1步,getExtensionClasses()

  1. private Map<String,Class<?>> getExtensionClasses() {
  2. //从缓存中获取插件类,第一次肯定没有
  3. Map<String,Class<?>> classes = cachedClasses.get();
  4. if (classes == null) {
  5. synchronized (cachedClasses) {
  6. classes = cachedClasses.get();
  7. if (classes == null) {
  8. //实际的加载插件方法
  9. classes = loadExtensionClasses();
  10. cachedClasses.set(classes);
  11. }
  12. }
  13. }
  14. return classes;
  15. }
  16. //ExtensionLoader中的三个常量,加载插件的目录,第一个熟悉吧,是java spi的默认目录
  17. private static final String SERVICES_DIRECTORY = "Meta-INF/services/";
  18. private static final String DUBBO_DIRECTORY = "Meta-INF/dubbo/";
  19. private static final String DUBBO_INTERNAL_DIRECTORY = DUBBO_DIRECTORY + "internal/";
  20. private Map<String,Class<?>> loadExtensionClasses() {
  21. //获取插口上SPI注解的值,默认值只能有一个,如果多于一个,则抛异常
  22. final SPI defaultAnnotation = type.getAnnotation(SPI.class);
  23. if (defaultAnnotation != null) {
  24. String value = defaultAnnotation.value();
  25. if ((value = value.trim()).length() > 0) {
  26. String[] names = NAME_SEPARATOR.split(value);
  27. if (names.length > 1) {
  28. throw new IllegalStateException("more than 1 default extension name on extension " + type.getName()
  29. + ": " + Arrays.toString(names));
  30. }
  31. if (names.length == 1) cachedDefaultName = names[0];
  32. }
  33. }
  34. //加载以上三个目录下的实现了相应插口的插件类(本例中插口是Protocol)
  35. Map<String,Class<?>> extensionClasses = new HashMap<String,Class<?>>();
  36. loadDirectory(extensionClasses,DUBBO_INTERNAL_DIRECTORY,type.getName());
  37. loadDirectory(extensionClasses,type.getName().replace("org.apache","com.alibaba"));
  38. loadDirectory(extensionClasses,DUBBO_DIRECTORY,SERVICES_DIRECTORY,"com.alibaba"));
  39. return extensionClasses;
  40. }

调试发现,共取到四个插件(实现Protocol接口的不止这四个类,还有redis、memcache等,不知为啥只取到这四个类?):

protocol插件

再来看上面getAdaptiveExtensionClass方法的第2步,这一句是判断有没有自适应类,在加载配置的插件过程中,会判断此插件类是不是自适应插件类,判断的依据就是插件类上是否有注解@Adaptive,Protocol的这四个插件类上都没有此注解,所以没有自适应插件,则会走到第3步,创建一个自适应插件

  1. private Class<?> createAdaptiveExtensionClass() {
  2. //生成代码
  3. String code = createAdaptiveExtensionClassCode();
  4. ClassLoader classLoader = findClassLoader();
  5. //得到编辑器,并将类代码编译成字节码
  6. org.apache.dubbo.common.compiler.Compiler compiler = ExtensionLoader.getExtensionLoader(org.apache.dubbo.common.compiler.Compiler.class).getAdaptiveExtension();
  7. return compiler.compile(code,classLoader);
  8. }
  9. //来看看生成代码的过程,以生成Protocol插件代码为例
  10. private String createAdaptiveExtensionClassCode() {
  11. StringBuilder codeBuilder = new StringBuilder();
  12. //得到Protocol接口所有方法
  13. Method[] methods = type.getMethods();
  14. boolean hasAdaptiveAnnotation = false;
  15. for (Method m : methods) {
  16. if (m.isAnnotationPresent(Adaptive.class)) {
  17. hasAdaptiveAnnotation = true;
  18. break;
  19. }
  20. }
  21. // // 如果方法上没有@Adaptive注解,则不能创建自适应插件
  22. if (!hasAdaptiveAnnotation)
  23. throw new IllegalStateException("No adaptive method on extension " + type.getName() + ",refuse to create the adaptive class!");
  24. codeBuilder.append("package ").append(type.getPackage().getName()).append(";");
  25. codeBuilder.append("\nimport ").append(ExtensionLoader.class.getName()).append(";");
  26. //类名为Protocol$Adaptive实现了Protocol接口
  27. codeBuilder.append("\npublic class ").append(type.getSimpleName()).append("$Adaptive").append(" implements ").append(type.getCanonicalName()).append(" {");
  28. for (Method method : methods) {
  29. Class<?> rt = method.getReturnType();
  30. Class<?>[] pts = method.getParameterTypes();
  31. Class<?>[] ets = method.getExceptionTypes();
  32. Adaptive adaptiveAnnotation = method.getAnnotation(Adaptive.class);
  33. StringBuilder code = new StringBuilder(512);
  34. if (adaptiveAnnotation == null) {
  35. code.append("throw new UnsupportedOperationException(\"method ")
  36. .append(method.toString()).append(" of interface ")
  37. .append(type.getName()).append(" is not adaptive method!\");");
  38. } else {
  39. int urlTypeIndex = -1;
  40. for (int i = 0; i < pts.length; ++i) {
  41. if (pts[i].equals(URL.class)) {
  42. urlTypeIndex = i;
  43. break;
  44. }
  45. }
  46. // 如果发现方法中的参数有一个URL类型
  47. if (urlTypeIndex != -1) {
  48. // Null Point check
  49. String s = String.format("\nif (arg%d == null) throw new IllegalArgumentException(\"url == null\");",urlTypeIndex);
  50. code.append(s);
  51. s = String.format("\n%s url = arg%d;",URL.class.getName(),urlTypeIndex);
  52. code.append(s);
  53. }
  54. // 如果没有发现,则会寻找每一个参数类型中的属性是否有为URL类型的
  55. else {
  56. String attribMethod = null;
  57. // find URL getter method
  58. LBL_PTS:
  59. for (int i = 0; i < pts.length; ++i) {
  60. Method[] ms = pts[i].getMethods();
  61. for (Method m : ms) {
  62. String name = m.getName();
  63. if ((name.startsWith("get") || name.length() > 3)
  64. && Modifier.isPublic(m.getModifiers())
  65. && !Modifier.isStatic(m.getModifiers())
  66. && m.getParameterTypes().length == 0
  67. && m.getReturnType() == URL.class) {
  68. urlTypeIndex = i;
  69. attribMethod = name;
  70. break LBL_PTS;
  71. }
  72. }
  73. }
  74. //如果没找到,则抛出异常
  75. if (attribMethod == null) {
  76. throw new IllegalStateException("fail to create adaptive class for interface " + type.getName()
  77. + ": not found url parameter or url attribute in parameters of method " + method.getName());
  78. }
  79. // Null point check
  80. String s = String.format("\nif (arg%d == null) throw new IllegalArgumentException(\"%s argument == null\");",urlTypeIndex,pts[urlTypeIndex].getName());
  81. code.append(s);
  82. s = String.format("\nif (arg%d.%s() == null) throw new IllegalArgumentException(\"%s argument %s() == null\");",attribMethod,pts[urlTypeIndex].getName(),attribMethod);
  83. code.append(s);
  84. s = String.format("%s url = arg%d.%s();",attribMethod);
  85. code.append(s);
  86. }
  87. String[] value = adaptiveAnnotation.value();
  88. // value is not set,use the value generated from class name as the key
  89. if (value.length == 0) {
  90. char[] charArray = type.getSimpleName().tocharArray();
  91. StringBuilder sb = new StringBuilder(128);
  92. for (int i = 0; i < charArray.length; i++) {
  93. if (Character.isUpperCase(charArray[i])) {
  94. if (i != 0) {
  95. sb.append(".");
  96. }
  97. sb.append(Character.toLowerCase(charArray[i]));
  98. } else {
  99. sb.append(charArray[i]);
  100. }
  101. }
  102. value = new String[]{sb.toString()};
  103. }
  104. boolean hasInvocation = false;
  105. for (int i = 0; i < pts.length; ++i) {
  106. if (pts[i].getName().equals("org.apache.dubbo.rpc.Invocation")) {
  107. // Null Point check
  108. String s = String.format("\nif (arg%d == null) throw new IllegalArgumentException(\"invocation == null\");",i);
  109. code.append(s);
  110. s = String.format("\nString methodName = arg%d.getMethodName();",i);
  111. code.append(s);
  112. hasInvocation = true;
  113. break;
  114. }
  115. }
  116. String defaultExtName = cachedDefaultName;
  117. String getNameCode = null;
  118. for (int i = value.length - 1; i >= 0; --i) {
  119. if (i == value.length - 1) {
  120. if (null != defaultExtName) {
  121. if (!"protocol".equals(value[i]))
  122. if (hasInvocation)
  123. getNameCode = String.format("url.getMethodParameter(methodName,\"%s\",\"%s\")",value[i],defaultExtName);
  124. else
  125. getNameCode = String.format("url.getParameter(\"%s\",defaultExtName);
  126. else
  127. getNameCode = String.format("( url.getProtocol() == null ? \"%s\" : url.getProtocol() )",defaultExtName);
  128. } else {
  129. if (!"protocol".equals(value[i]))
  130. if (hasInvocation)
  131. getNameCode = String.format("url.getMethodParameter(methodName,defaultExtName);
  132. else
  133. getNameCode = String.format("url.getParameter(\"%s\")",value[i]);
  134. else
  135. getNameCode = "url.getProtocol()";
  136. }
  137. } else {
  138. if (!"protocol".equals(value[i]))
  139. //如果方法参数类型名称为"org.apache.dubbo.rpc.Invocation"则从url获取以此参数类型名为key的值,获取不到则取默认扩展名,即Protocol接口上注解SPI的值“dubbo”
  140. if (hasInvocation)
  141. getNameCode = String.format("url.getMethodParameter(methodName,defaultExtName);
  142. else
  143. //否则,取从url中取以方法上注解adaptive的值为key对应的值
  144. getNameCode = String.format("url.getParameter(\"%s\",%s)",getNameCode);
  145. else
  146. getNameCode = String.format("url.getProtocol() == null ? (%s) : url.getProtocol()",getNameCode);
  147. }
  148. }
  149. code.append("\nString extName = ").append(getNameCode).append(";");
  150. // check extName == null?
  151. String s = String.format("\nif(extName == null) " +
  152. "throw new IllegalStateException(\"Fail to get extension(%s) name from url(\" + url.toString() + \") use keys(%s)\");",type.getName(),Arrays.toString(value));
  153. code.append(s);
  154. s = String.format("\n%s extension = (%<s)%s.getExtensionLoader(%s.class).getExtension(extName);",ExtensionLoader.class.getSimpleName(),type.getName());
  155. code.append(s);
  156. // return statement
  157. if (!rt.equals(void.class)) {
  158. code.append("\nreturn ");
  159. }
  160. s = String.format("extension.%s(",method.getName());
  161. code.append(s);
  162. for (int i = 0; i < pts.length; i++) {
  163. if (i != 0)
  164. code.append(",");
  165. code.append("arg").append(i);
  166. }
  167. code.append(");");
  168. }
  169. codeBuilder.append("\npublic ").append(rt.getCanonicalName()).append(" ").append(method.getName()).append("(");
  170. for (int i = 0; i < pts.length; i++) {
  171. if (i > 0) {
  172. codeBuilder.append(",");
  173. }
  174. codeBuilder.append(pts[i].getCanonicalName());
  175. codeBuilder.append(" ");
  176. codeBuilder.append("arg").append(i);
  177. }
  178. codeBuilder.append(")");
  179. if (ets.length > 0) {
  180. codeBuilder.append(" throws ");
  181. for (int i = 0; i < ets.length; i++) {
  182. if (i > 0) {
  183. codeBuilder.append(",");
  184. }
  185. codeBuilder.append(ets[i].getCanonicalName());
  186. }
  187. }
  188. codeBuilder.append(" {");
  189. codeBuilder.append(code.toString());
  190. codeBuilder.append("\n}");
  191. }
  192. codeBuilder.append("\n}");
  193. if (logger.isDebugEnabled()) {
  194. logger.debug(codeBuilder.toString());
  195. }
  196. return codeBuilder.toString();
  197. }

我们来看下生成插件类Protocol$Adaptive代码:

  1. package org.apache.dubbo.rpc;
  2. import org.apache.dubbo.common.extension.ExtensionLoader;
  3. import org.apache.dubbo.common.URL;
  4. import org.apache.dubbo.rpc.Protocol ;
  5. import org.apache.dubbo.rpc.RpcException;
  6. import org.apache.dubbo.rpc.Invoker ;
  7. import org.apache.dubbo.rpc.Exporter;
  8. public class Protocol$Adaptive implements Protocol {
  9. public void destroy(){
  10. throw new UnsupportedOperationException("method public abstract void org.apache.dubbo.rpc.Protocol.destroy() of interface org.apache.dubbo.rpc.Protocol is not adaptive method!");
  11. }
  12. public int getDefaultPort() {
  13. throw new UnsupportedOperationException("method public abstract int org.apache.dubbo.rpc.Protocol.getDefaultPort() of interface org.apache.dubbo.rpc.Protocol is not adaptive method!");
  14. }
  15. public Invoker refer(Class arg0,URL arg1) throws RpcException {
  16. if (arg1 == null)
  17. throw new IllegalArgumentException("url == null");
  18. URL url = arg1;
  19. String extName = ( url.getProtocol() == null ? "dubbo" : url.getProtocol() );
  20. if(extName == null)
  21. throw new IllegalStateException("Fail to get extension(org.apache.dubbo.rpc.Protocol) name from url(" + url.toString() + ") use keys([protocol])");
  22. Protocol extension = (Protocol)ExtensionLoader.getExtensionLoader(Protocol.class).getExtension(extName);
  23. return extension.refer(arg0,arg1);
  24. }
  25. public Exporter export(Invoker arg0) throws RpcException {
  26. if (arg0 == null)
  27. throw new IllegalArgumentException("org.apache.dubbo.rpc.Invoker argument == null");
  28. if (arg0.getUrl() == null)
  29. throw new IllegalArgumentException("org.apache.dubbo.rpc.Invoker argument getUrl() == null");org.apache.dubbo.common.URL url = arg0.getUrl();
  30. String extName = ( url.getProtocol() == null ? "dubbo" : url.getProtocol() );
  31. if(extName == null)
  32. throw new IllegalStateException("Fail to get extension(org.apache.dubbo.rpc.Protocol) name from url(" + url.toString() + ") use keys([protocol])");
  33. Protocol extension = (org.apache.dubbo.rpc.Protocol)ExtensionLoader.getExtensionLoader(org.apache.dubbo.rpc.Protocol.class).getExtension(extName);
  34. return extension.export(arg0);
  35. }
  36. }

可以看到Protocol$Adaptive可以根据url中参数protocol值加载对应的插件,如果url中没有,则加载名为"dubbo"对应的插件,而从前面加载的四个插件可以看出,名称为dubbo的插件类为org.apache.dubbo.rpc.protocol.dubbo.DubboProtocol.

写到这里总算将SPI加载的过程大体上讲述了一篇,Dubbo中还有许多类似的插件,原理基本相同;除了有的插口有自适应插件,比如org.apache.dubbo.common.compiler.Compilerorg.apache.dubbo.common.extension.ExtensionFactory,自适应插件类上都有注解@Adaptive,比如Compile的自适应插件AdaptiveCompiler,ExtensionFactory的自适应插件AdaptiveExtensionFactory.

为什么要提供自适应插件,而不是都在运行时生成
答:
(1)解决鸡生蛋,蛋生鸡的问题,上面createAdaptiveExtensionClass方法中,在第1步生成Protocol$Adaptive类后,会使用编译器将其编译成字节码,但是编译器本身也是插件化的,可以有好几种编译器,所以需要提供一个已经存在的自适应编译器(AdaptiveCompiler),然后在编译的时候,使用此编译器找到Compile接口上SPI注解中配置的默认的编译器进行编译。
(2)解决对象生成方式不同导致的加载问题;Dubbo中对象的生成一类是由Spring容器创建,一类是根据插件文件的配置动态加载;所以要想获取这两部分对象,需要使用不同的方式;而AdaptiveExtensionFactory就是为了解决这个问题,在获取对象时,分别从Spring容器和ExtensionLoader中查找。