Dubbo之provider bean注册详解

原创
2019/05/06 09:45
阅读数 4.6K

       在最新版的Dubbo中,service bean的注册是可以使用注解方式进行的,声明方式是在目标bean上使用@org.apache.dubbo.config.annotation.Service(注意包路径与spring的@Service不同,后文说道的@Service注解都是指此Dubbo路径的注解)注解进行标注即可。使用该注解进行标注之后,当前bean就会被注册为spring容器所管理的bean,并且能够对外提供远程调用。本文主要讲解Dubbo是如何对这些bean进行注册的。

       对于@Service标注的类的注册,Dubbo主要是通过ServiceAnnotationBeanPostProcessor来实现的,该类实现了BeanDefinitionRegistryPostProcessor接口,其postProcessBeanDefinitionRegistry()方法会在当前容器中默认的BeanDefinition注册完毕后调用。Dubbo就是通过在该方法中将使用@Service标注的类的实例注册到Spring容器中的。我们首先看看该方法的源码:

 @Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry)
      throws BeansException {

  // packagesToScan中保存了指定的要扫描的dubbo类路径,这里主要是对这些路径中的
  // 占位符进行处理,以将其替换为相应的值
  Set<String> resolvedPackagesToScan = resolvePackagesToScan(packagesToScan);

  if (!CollectionUtils.isEmpty(resolvedPackagesToScan)) {
    // 这里主要是进行了两个动作,一个是扫描当前类路径中所有标注了
    // @org.apache.dubbo.config.annotation.Service注解的类,将其当做一个bean进行注册;
    // 另一个是为每一个目标类注册一个引用了该类的ServiceBean的BeanDefinition,这些ServiceBean是
    // Dubbo能够对外提供远程服务调用的关键,关于其实现原理,后面我们将详细讲解
    registerServiceBeans(resolvedPackagesToScan, registry);
  } else {
    if (logger.isWarnEnabled()) {
      logger.warn("packagesToScan is empty , ServiceBean registry will be ignored!");
    }
  }

}

       可以看到,这里首先是获取配置的需要扫描Dubbo bean的路径,并且对该路径中的占位符进行处理。然后将具体的注册动作委托给registerServiceBeans()方法进行,下面我们继续阅读该方法的源码:

private void registerServiceBeans(Set<String> packagesToScan, 
      BeanDefinitionRegistry registry) {
  // 创建一个DubboClassPathBeanDefinitionScanner,该类的作用主要是扫描classpath中指定路径
  // 下的所有class文件,根据其filter指定的条件判断扫描到的class文件是否符合当前条件,
  // 最后将符合条件的class封装为一个BeanDefinition注册到BeanDefinitionRegistry中
  DubboClassPathBeanDefinitionScanner scanner =
    new DubboClassPathBeanDefinitionScanner(registry, environment, resourceLoader);
  // 获取一个BeanNameGenerator,主要作用是为目标类创建要给bean名称
  BeanNameGenerator beanNameGenerator = resolveBeanNameGenerator(registry);
  scanner.setBeanNameGenerator(beanNameGenerator);
  // 添加一个filter,这里添加的filter就是如果目标类上标注了@Service注解,
  // 那么就会将其注册到BeanDefinitionRegistry中
  scanner.addIncludeFilter(new AnnotationTypeFilter(Service.class));

  for (String packageToScan : packagesToScan) {
    // 对每一个文件夹进行扫描,判断扫描到的class是否符合filter指定的条件,
    // 符合则将其封装为一个BeanDefinition,并且注册到BeanDefinitionRegistry中
    scanner.scan(packageToScan);
    // 查找所有前一步注册的Dubbo Service bean
    Set<BeanDefinitionHolder> beanDefinitionHolders =
      findServiceBeanDefinitionHolders(scanner, packageToScan, registry, beanNameGenerator);

    if (!CollectionUtils.isEmpty(beanDefinitionHolders)) {
      for (BeanDefinitionHolder beanDefinitionHolder : beanDefinitionHolders) {
        // 为每一个注册的使用@Service标注的bean声明一个ServiceBean的BeanDefinition对象,
        // 并且指定其ref属性为当前bean,该ServiceBean会将其引用的bean注册为一个
        // 可用于远程调用的bean
        registerServiceBean(beanDefinitionHolder, registry, scanner);
      }
    }
  }
}

       上述registerServiceBeans()就是注册Dubbo provider bean的主要流程方法,简要来说就是分为两步:

  • 查找指定目录下使用@Service注解标注的所有class文件,将该class封装为一个BeanDefinition,并且注册到BeanDefinitionRegistry中;
  • 为每一个@Service注解标注的bean注册一个ServiceBean,指定其ref属性为@Service注解标注的bean;

1. 封装并注册BeanDefinition

        这里我们首先看DubboClassPathBeanDefinitionScanner.scan()方法是如何扫描目标路径,并且注册相关bean的。如下是该方法的源码:

public int scan(String... basePackages) {
  // 获取当前已经注册的BeanDefinition数量
  int beanCountAtScanStart = this.registry.getBeanDefinitionCount();
  // 扫描指定路径下的Dubbo bean
  doScan(basePackages);
  if (this.includeAnnotationConfig) {
    // 注册用于处理诸如@Configuration,@Autowired,@Required等注解的类或属性的Processor
    AnnotationConfigUtils.registerAnnotationConfigProcessors(this.registry);
  }

  // 返回注册的Dubbo provider BeanDefinition的数量
  return (this.registry.getBeanDefinitionCount() - beanCountAtScanStart);
}

protected Set<BeanDefinitionHolder> doScan(String... basePackages) {
  Assert.notEmpty(basePackages, "At least one base package must be specified");
  Set<BeanDefinitionHolder> beanDefinitions = new LinkedHashSet<BeanDefinitionHolder>();
  for (String basePackage : basePackages) {
    // 在指定目录下查找所有的符合条件的class,并且将其封装为一个BeanDefinition对象
    Set<BeanDefinition> candidates = findCandidateComponents(basePackage);
    for (BeanDefinition candidate : candidates) {
      // 检查目标类是否标注了@Scope注解,如果标注了该注解,则将其属性封装为一个ScopeMetadata,
      // 并且将配置的scope名称设置到当前BeanDefinition中
      ScopeMetadata scopeMetadata = scopeMetadataResolver.resolveScopeMetadata(candidate);
      candidate.setScope(scopeMetadata.getScopeName());
      // 按照指定的策略为当前BeanDefinition生成一个名称
      String beanName = this.beanNameGenerator.generateBeanName(candidate, this.registry);
      if (candidate instanceof AbstractBeanDefinition) {
        // 为当前BeanDefinition设置lazyInit,autowireMode等属性的默认值
        postProcessBeanDefinition((AbstractBeanDefinition) candidate, beanName);
      }
      if (candidate instanceof AnnotatedBeanDefinition) {
        // 检查当前Bean是否标注了诸如@Lazy,@Primary等注解,如果标注了,则获取其值,
        // 并且将其设置到当前的BeanDefinition中
        AnnotationConfigUtils
          .processCommonDefinitionAnnotations((AnnotatedBeanDefinition) candidate);
      }
      
      // 检查当前BeanDefinitionRegistry中是否已经存在该名称的bean,如果已经存在,
      // 则表示当前BeanDefinition不能进行注册,但这里会判断已经存在的BeanDefinition
      // 与当前BeanDefinition是不是匹配的,如果不是匹配的, 则抛出异常,如果是匹配的,
      // 说明两者是同一个BeanDefinition,则返回false,而不对当前BeanDefinition进行处理
      if (checkCandidate(beanName, candidate)) {
        // 将当前BeanDefinition封装为一个BeanDefinitionHolder
        BeanDefinitionHolder definitionHolder = 
          new BeanDefinitionHolder(candidate, beanName);
        definitionHolder = AnnotationConfigUtils
          .applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry);
        beanDefinitions.add(definitionHolder);
        // 将封装得到的BeanDefinitionHolder注册到BeanDefinitionRegistry中
        registerBeanDefinition(definitionHolder, this.registry);
      }
    }
  }
  return beanDefinitions;
}

        在doScan()方法中,首先通过findCandidateComponents()方法查找指定目录下的所有符合条件的class,并且将其封装为一个BeanDefinition,然后为得到的BeanDefinition设置默认的属性,最后将其注册到BeanDefinitionRegistry中。这里我们主要看findCandidateComponents()是如何查找并封装目标BeanDefinition的:

public Set<BeanDefinition> findCandidateComponents(String basePackage) {
  Set<BeanDefinition> candidates = new LinkedHashSet<BeanDefinition>();
  try {
    // 这里resolveBasePackage()方法会对目标路径中的占位符进行处理,以替换为真实的路径。
    // 这里进行组装的方式其实就是在目标路径前面添加classpath*:,并且在结尾加上**/*.class,
    // 处理后的路径形如:classpath*:com/xiaowanzi/service/**/*.class,这样处理之后,
    // 我们就可以使用Ant的方式将目标类路径与该路径进行匹配,如果匹配上了,就说明是需要进行后续处理的类
    String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX +
      resolveBasePackage(basePackage) + '/' + this.resourcePattern;
    // 这里主要就是获取目标路径下的所有class文件,并且将其封装为Resource对象
    Resource[] resources = resourcePatternResolver.getResources(packageSearchPath);
    for (Resource resource : resources) {
      if (resource.isReadable()) {
        try {
          // 这里的MetadataReader就是用于读取当前Resource代表的类的一些元数据的,
          // 比如该类所标注的注解
          MetadataReader metadataReader = metadataReaderFactory.getMetadataReader(resource);
          // isCandidate()方法本质上就是通过之前为当前Scanner设置的includeFilter和excludeFilter
          // 判断当前Resource是否符合这些filter所设置的条件,只有符合条件的Resource才是我们最终
          // 要将其封装为BeanDefinition的类
          if (isCandidateComponent(metadataReader)) {
            // 这里说明当前Resource代表的class是一个目标class,那么就将其封装为一个BeanDefinition
            ScannedGenericBeanDefinition sbd = 
              new ScannedGenericBeanDefinition(metadataReader);
            sbd.setResource(resource);
            sbd.setSource(resource);
            // 判断当前class类是一个具体的类,或者其是抽象的,并且标注有@Lookup注解,用于指定子类
            if (isCandidateComponent(sbd)) {
              candidates.add(sbd);	// 将当前类添加到结果集中
            }
          }
        } catch (Throwable ex) {
          throw new BeanDefinitionStoreException(
            "Failed to read candidate component class: " + resource, ex);
        }
      }
    }
  } catch (IOException ex) {
    throw new BeanDefinitionStoreException("I/O failure during classpath scanning", ex);
  }
  return candidates;
}

       可以看到,这里findCandidateComponents()的主要作用就是首先查找指定目录下的资源文件,然后依次判断该资源文件是否符合当前Scanner所指定的filter条件,如果符合,则将其封装为一个BeanDefinition,并且添加到结果集中返回。这里我们继续看resourcePatternResolver.getResources()是如何查找资源文件的,如下是该方法的源码:

// GenericApplicationContext.getResources()最终将查找动作委托给了
// PathMatchingResourcePatternResolver.getResources()方法,我们直接看该方法的源码
public Resource[] getResources(String locationPattern) throws IOException {
  Assert.notNull(locationPattern, "Location pattern must not be null");
  // 判断目标路径是否以classpath*:开始,是的则以读取classpath文件的方式进行处理
  if (locationPattern.startsWith(CLASSPATH_ALL_URL_PREFIX)) {
    // 如果目标路径是Ant形式的,那么就以Ant形式进行匹配。这里Ant形式指的是类路径中包含*或者?
    if (getPathMatcher()
        .isPattern(locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length()))) {
      // 通过Ant模式来匹配指定路径下的资源文件
      return findPathMatchingResources(locationPattern);
    } else {
      // 如果不是Ant形式,那么就将其当做一个路径前缀来进行处理,这里会直接读取该路径下的所有文件返回
      return findAllClassPathResources(
        locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length()));
    }
  } else {
    // 对路径进行处理,如果是war:开头,说明其是Tomcat协议中的写法,那么就只取*/后的部分,
    // 否则取:后面的部分
    int prefixEnd = (locationPattern.startsWith("war:") 
        ? locationPattern.indexOf("*/") + 1 : locationPattern.indexOf(':') + 1);
    // 如果路径是Ant形式的路径,则使用Ant的方式在目标目录下查找对应的资源文件
    if (getPathMatcher().isPattern(locationPattern.substring(prefixEnd))) {
      return findPathMatchingResources(locationPattern);
    } else {
      // 如果路径不是Ant形式的,则将其当做一个全路径来处理,那么直接读取该路径下的文件
      return new Resource[] {getResourceLoader().getResource(locationPattern)};
    }
  }
}

        这里主要是根据路径的不同形式来使用不同的方式读取路径下的资源文件,由于前面已经对路径添加了Ant形式的后缀,因而Dubbo是使用Ant的形式对路径进行匹配,因而我们继续阅读findPathMatchingResources()方法的源码:

protected Resource[] findPathMatchingResources(String locationPattern) throws IOException {
  // 这里rootDirPath就是获取当前Ant形式路径的根路径,所谓的根路径就是不包含Ant通配符的路径前缀部分。
  // 比如classpath*:com/xiaowanzi/service/**/*.class的根路径是
  // classpath*:com/xiaowanzi/service
  String rootDirPath = determineRootDir(locationPattern);
  // 子模式就是整个路径去除根路径的包含Ant通配符的部分,比如上面的**/*.class
  String subPattern = locationPattern.substring(rootDirPath.length());
  // 获取根路径下的所有资源文件
  Resource[] rootDirResources = getResources(rootDirPath);
  Set<Resource> result = new LinkedHashSet<Resource>(16);
  for (Resource rootDirResource : rootDirResources) {
    // 这里只是一个hook方法,并没有做任何处理
    rootDirResource = resolveRootDirResource(rootDirResource);
    URL rootDirUrl = rootDirResource.getURL();
    // 如果equinoxResolveMethod不为空,则通过该方法加载该资源文件,
    // 这里equinoxResolveMethod是org.eclipse.core.runtime.FileLocator.resolve()方法,
    // 这里其实就是判断当前工程中是否存在该类,以便借助该类进行处理
    if (equinoxResolveMethod != null) {
      if (rootDirUrl.getProtocol().startsWith("bundle")) {
        rootDirUrl = (URL) ReflectionUtils
          .invokeMethod(equinoxResolveMethod, null, rootDirUrl);
        rootDirResource = new UrlResource(rootDirUrl);
      }
    }
    
    // 如果目标资源是vfs文件,则交由VfsResourceMatchingDelegate读取该资源文件
    if (rootDirUrl.getProtocol().startsWith(ResourceUtils.URL_PROTOCOL_VFS)) {
      result.addAll(VfsResourceMatchingDelegate
         .findMatchingResources(rootDirUrl, subPattern, getPathMatcher()));
      // 如果目标资源存在于jar包中,则以jar包的形式读取资源文件
    } else if (ResourceUtils.isJarURL(rootDirUrl) || isJarResource(rootDirResource)) {
      result.addAll(
        doFindPathMatchingJarResources(rootDirResource, rootDirUrl, subPattern));
    } else {
      // 这里说明目标资源文件是普通的存在与当前classpath中的class文件,那么就直接读取该资源文件
      result.addAll(doFindPathMatchingFileResources(rootDirResource, subPattern));
    }
  }
  
  // 将资源文件转换为一个数组返回
  return result.toArray(new Resource[result.size()]);
}

        这里查找资源文件的方式就是查找指定路径下的所有文件,得到的一个一系列的URL对象,然后对这些URL对象进行判断,按照其存储的不同的形式进行读取。这里我们以查找类路径下的普通class文件的方式进行讲解,如下是doFindPathMatchingFileResources()方法的源码:

protected Set<Resource> doFindPathMatchingFileResources(Resource rootDirResource, 
      String subPattern) throws IOException {
  // 根据根路径获取其对应的File文件目录句柄
  File rootDir = rootDirResource.getFile().getAbsoluteFile();
  // 获取根目录下的所有文件,并且对其进行匹配
  return doFindMatchingFileSystemResources(rootDir, subPattern);
}

protected Set<Resource> doFindMatchingFileSystemResources(File rootDir, String subPattern) throws IOException {
  // 获取根路径下所有匹配的文件
  Set<File> matchingFiles = retrieveMatchingFiles(rootDir, subPattern);
  Set<Resource> result = new LinkedHashSet<Resource>(matchingFiles.size());
  for (File file : matchingFiles) {
    // 将匹配的文件封装为一个FileSystemResource对象,然后将其添加到结果集中
    result.add(new FileSystemResource(file));
  }
  return result;
}

protected Set<File> retrieveMatchingFiles(File rootDir, String pattern) throws IOException {
  // 如果根目录文件不存在,则直接返回
  if (!rootDir.exists()) {
    return Collections.emptySet();
  }
  
  // 如果根目录文件不是一个目录,则直接返回
  if (!rootDir.isDirectory()) {
    return Collections.emptySet();
  }
  
  // 如果根目录文件不可读,则直接返回
  if (!rootDir.canRead()) {
    return Collections.emptySet();
  }
  
  // 将根目录文件的路径中的文件分隔符全部替换为反斜杠“/”,以便与目标模式路径进行匹配
  String fullPattern = StringUtils.replace(rootDir.getAbsolutePath(), File.separator, "/");
  if (!pattern.startsWith("/")) {
    fullPattern += "/";
  }
  
  // 对模式路径进行处理,将其目录分隔符替换为反斜杠“/”
  fullPattern = fullPattern + StringUtils.replace(pattern, File.separator, "/");
  Set<File> result = new LinkedHashSet<File>(8);
  // 获取目标根目录下的所有文件,并且根据模式路径对这些文件进行匹配
  doRetrieveMatchingFiles(fullPattern, rootDir, result);
  return result;
}

protected void doRetrieveMatchingFiles(String fullPattern, File dir, Set<File> result) 
      throws IOException {
  // 获取目标目录下的所有文件
  File[] dirContents = dir.listFiles();
  if (dirContents == null) {
    return;
  }
  
  Arrays.sort(dirContents);
  for (File content : dirContents) {
    String currPath = StringUtils.replace(content.getAbsolutePath(), File.separator, "/");
    // 如果当前获取到的子文件还是一个目录,则递归的调用当前方法以获取该目录下的文件
    if (content.isDirectory() && getPathMatcher().matchStart(fullPattern, currPath + "/")) {
      if (content.canRead()) {
        doRetrieveMatchingFiles(fullPattern, content, result);
      }
    }
    // 如果当前文件不是一个目录,则将当前文件路径与模式路径进行匹配,如果匹配上了,则将其添加到结果集中
    if (getPathMatcher().match(fullPattern, currPath)) {
      result.add(content);
    }
  }
}

       可以看到,这里就是进行匹配的主要流程,主要就是查找指定目录下的所有文件,并且将该文件的路径与设置的模式路径进行匹配,匹配上了则说明该文件是我们所需要的文件,此时会将其添加到结果集中返回。

        前面我们讲到,在获取所有的资源文件之后,会将资源文件封装为一个MetadataReader对象,然后判断其是否符合当前Scanner所设置的filter条件,符合条件的才是我们所需要的class。这里的判断过程在Scanner.isCandidateComponent()方法中,如下是该方法的源码:

protected boolean isCandidateComponent(MetadataReader metadataReader) throws IOException {
  // 通过excludeFilters判断当前MetadataReader是否需要被过滤掉,是则返回false,表示应该被排除
  for (TypeFilter tf : this.excludeFilters) {
    if (tf.match(metadataReader, this.metadataReaderFactory)) {
      return false;
    }
  }
  
  // 通过includeFilter判断当前MetadataReader是否是符合条件的,是则判断其是否标注了@Conditional
  // 注解,如果标注了该注解,则根据该注解所指定的条件,判断当前class是否符合该条件,符合才说明当前
  // class是我们所需要的class文件
  for (TypeFilter tf : this.includeFilters) {
    if (tf.match(metadataReader, this.metadataReaderFactory)) {
      return isConditionMatch(metadataReader);
    }
  }
  return false;
}

       由于我们在声明Scanner对象时,只指定了includeFilter为AnnotationTypeFilter,并且指定了注解为Service,我们这里直接看其match()方法:

@Override
public boolean match(MetadataReader metadataReader, 
      MetadataReaderFactory metadataReaderFactory) throws IOException {
  // matchSelf()就是判断当前class是否标注了目标注解,这里是@Service,如果标注了,则返回true
  if (matchSelf(metadataReader)) {
    return true;
  }
  ClassMetadata metadata = metadataReader.getClassMetadata();
  if (matchClassName(metadata.getClassName())) {	// 该方法默认返回false,主要是提供给子类实现的
    return true;
  }

  // 如果设置了考虑父类,则会递归的判断各个父类是否有标注目标注解,如果标注了,则返回true
  if (this.considerInherited) {
    if (metadata.hasSuperClass()) {
      Boolean superClassMatch = matchSuperClass(metadata.getSuperClassName());
      if (superClassMatch != null) {
        if (superClassMatch.booleanValue()) {
          return true;
        }
      } else {
        if (match(metadata.getSuperClassName(), metadataReaderFactory)) {
          return true;
        }
      }
    }
  }

  // 如果设置了考虑接口,则会递归的判断各个接口是否有标注目标注解,如果标注了,则返回true
  if (this.considerInterfaces) {
    for (String ifc : metadata.getInterfaceNames()) {
      Boolean interfaceMatch = matchInterface(ifc);
      if (interfaceMatch != null) {
        if (interfaceMatch.booleanValue()) {
          return true;
        }
      } else {
        if (match(ifc, metadataReaderFactory)) {
          return true;
        }
      }
    }
  }

  return false;
}

        可以看到,这里就是对目标bean进行过滤的主要逻辑,其判断方式就是通过查找目标class上是否标注了@Service注解来进行,如果标注了,则是一个目标bean。通过这里的判断的class文件,最后会被封装为一个BeanDefinitionHolder,然后注册到BeanDefinitionRegistry中。

2. ServiceBean注册

        对于ServiceBean,其是Dubbo提供对外服务的核心,该类会将每一个Dubbo类型的bean都注册到zookeeper上,以便其他的服务通过zookeeper获取该类的信息,然后通过TCP协议进行远程调用。关于ServiceBean的工作原理,我们后续会进行详细讲解,这里主要讲解其是如何注册到BeanDefinitionRegistry中的。

        前面ServiceAnnotationBeanPostProcessor.registerServiceBeans()方法中,在注册了所有@Service声明的所有BeanDefinition之后,会通过findServiceBeanDefinitionHolders()方法查找得到所有这些BeanDefinition,然后在registerServiceBean()方法中为这每一个BeanDefinition创建一个ServiceBean的BeanDefinition,并且其ref属性指向了这些@Service标注的class对应的实例。这里我们主要查看其是如何进行ServiceBean的注册的:

private void registerServiceBean(BeanDefinitionHolder beanDefinitionHolder,
      BeanDefinitionRegistry registry, DubboClassPathBeanDefinitionScanner scanner) {
  // 获取目标bean的class对象
  Class<?> beanClass = resolveClass(beanDefinitionHolder);
  // 查找该class上标注的@Service注解对象
  Service service = findAnnotation(beanClass, Service.class);
  // 获取目标bean所实现的接口对象
  Class<?> interfaceClass = resolveServiceInterfaceClass(beanClass, service);
  String annotatedServiceBeanName = beanDefinitionHolder.getBeanName();
  // 根据@Service注解中的各个属性,为ServiceBean构造一个BeanDefinition对象
  AbstractBeanDefinition serviceBeanDefinition =
    buildServiceBeanDefinition(service, interfaceClass, annotatedServiceBeanName);

  // 为当前ServiceBean生成一个名称
  String beanName = generateServiceBeanName(service, interfaceClass, annotatedServiceBeanName);

  // 判断当前BeanDefinitionRegistry中是否已经存在了当前名称的bean,如果存在,则不进行注册
  if (scanner.checkCandidate(beanName, serviceBeanDefinition)) {
    // 将当前ServiceBean对应的BeanDefinition注册到BeanDefinitionRegistry中
    registry.registerBeanDefinition(beanName, serviceBeanDefinition);
  }
}

       这里封装ServiceBean的过程,主要就是读取目标class文件上的@Service注解所设置的各个属性值,然后根据该属性值将其封装为一个BeanDefinition对象,并且其class设置为ServiceBean。封装完成后,就为当前ServiceBean对应的BeanDefinition生成一个名称,并且将其注册到BeanDefinitionRegistry中。

3. 小结

        本文首先从源码的角度讲解了Dubbo是如何生成并且注册provider bean对应的BeanDefinition的,然后讲解了将provider bean封装为ServiceBean对应的BeanDefinition的过程。

4. 广告

       读者朋友如果觉得本文还不错,可以点击下面的广告链接,这可以为作者带来一定的收入,从而激励作者创作更好的文章,非常感谢!

在项目开发过程中,企业会有很多的任务、需求、缺陷等需要进行管理,CORNERSTONE 提供敏捷、任务、需求、缺陷、测试管理、WIKI、共享文件和日历等功能模块,帮助企业完成团队协作和敏捷开发中的项目管理需求;更有甘特图、看板、思维导图、燃尽图等多维度视图,帮助企业全面把控项目情况。

展开阅读全文
加载中
点击加入讨论🔥(6) 发布并加入讨论🔥
打赏
6 评论
6 收藏
2
分享
返回顶部
顶部