7-基于Spring的框架-MyBatis——7-3 扩展及高级用法

概要

过度

前一篇文章介绍了Spring对MyBatis的一些接管,通过这种接管,简化了MyBatis配置的读取、SqlSession的创建、Mapper的生成。使我们能像使用普通Bean一样使用包装好的数据库连接操作API。但是还存在着一个问题:

在生产环境中,尤其是复杂的业务场景,常常存在大量的表,或者要抽离出大量的数据库封装类,一个一个在Spring中注册Mapper太慢了。

我们在第一篇文章中介绍了一种方法,就是直接注册一个扫描器,他会帮助我们将制定包下面的类扫描,并完成BD的注册。本文主要介绍一下Spring的类扫描的基本实现。

内容简介

介绍MyBatis是如何扫描指定包下面的类并完成BD注册的。

所属环节

介绍MyBatis包扫描功能的实现,并给予此介绍Spring的包扫描功能。

上下环节

上文: 介绍Spring集成MyBatis时对基本API的封装

下文: 无

源码解析

入口


  

这里是我们启用Spring类扫描功能的配置项,它注册了一个特殊的Bean,并指明了要扫描的包。我们看一下这个类的继承关系,方便分析它的工作原理:

7-基于Spring的框架-MyBatis——7-3 扩展及高级用法_第1张图片
1.png

有三个方向:

  1. 实现了ApplicationContextAware,原因大概猜一下:获得上下文才能扫描到类后进行注册
  2. 实现了BeanNameAware,不清楚要拿到自己BD的id有什么目的,后面注意看看
  3. 实现了 InitializingBean,这个初始化钩子可能要做一些基本的配置校验或者其他的逻辑
  4. 实现了BeanFactoryPostProcessor,我的理解,如果你要扫描包并进行BD的注册,最好保证在所有实例初始化之前做,这样能保证所有的BD都注册上,不会存在找不到的情况;这里是不是很熟悉,ApplicationContext的初始化步骤——在完成BD的注册后会找出BeanFactory的后处理器,并对BeanFactory进行调用处理。

除了BeanNameAware还是一脸懵逼之外,其他的我感觉我猜了个七七八八吧。感觉它继承的父类BeanDefinitiopnRegistryPostProcessor这里有大量逻辑。我们前面介绍过这个接口和BeanFactoryPostProcessor接口的关系和区别。

我们从简单开始,防止直接看核心的跟不上趟。

简单功能

ApplicationContextAware的实现:

@Override
public void setApplicationContext(ApplicationContext applicationContext) {
  this.applicationContext = applicationContext;
}

BeanNameAware的实现:

public void setBeanName(String name) {
  this.beanName = name;
}

InitializingBean的实现:

@Override
public void afterPropertiesSet() throws Exception {
  notNull(this.basePackage, "Property 'basePackage' is required");
}

这里只是校验了一个字段。

我们干脆梳理一下这个类的字段吧:

/**
 * 要扫描的包的地址,必传
 **/
private String basePackage;

/**
 * 
 **/
private boolean addToConfig = true;

/**
 * 获得 MyBatis 的sqlSessionFactory,方便生成、管理 sqlSession
 **/
private SqlSessionFactory sqlSessionFactory;

/**
 * 
 **/
private SqlSessionTemplate sqlSessionTemplate;

/**
 * 
 **/
private String sqlSessionFactoryBeanName;

/**
 * 
 **/
private String sqlSessionTemplateBeanName;

/**
 * 要扫描的注解名称
 **/
private Class annotationClass;

/**
 * 
 **/
private Class markerInterface;

private ApplicationContext applicationContext;

private String beanName;

/**
 * 是否要先调用一遍属性读取的操作,防止配置中有对变量的引用
 **/
private boolean processPropertyPlaceHolders;

/**
 * BD的id生成器
 **/
private BeanNameGenerator nameGenerator;

整体上很多字段都说不清,但是都是可以通过 Bean 的配置注入进来的,我们看他的逻辑,然后根据逻辑猜一下。

包扫描+BD注册

public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
  // 是否要先调用一遍属性读取的操作,如果设置了就先加载一下
  if (this.processPropertyPlaceHolders) {
    processPropertyPlaceHolders();
  }

  ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);
  scanner.setAddToConfig(this.addToConfig);
  scanner.setAnnotationClass(this.annotationClass);
  scanner.setMarkerInterface(this.markerInterface);
  scanner.setSqlSessionFactory(this.sqlSessionFactory);
  scanner.setSqlSessionTemplate(this.sqlSessionTemplate);
  scanner.setSqlSessionFactoryBeanName(this.sqlSessionFactoryBeanName);
  scanner.setSqlSessionTemplateBeanName(this.sqlSessionTemplateBeanName);
  scanner.setResourceLoader(this.applicationContext);
  scanner.setBeanNameGenerator(this.nameGenerator);
  scanner.registerFilters();
  // 将扫描工作委托
  scanner.scan(StringUtils.tokenizeToStringArray(this.basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS));
}

资源加载

我们先看一下资源加载的逻辑:

private void processPropertyPlaceHolders() {
  // 拿到 PropertyResourceConfigurer 类型的BD定义
  Map prcs = applicationContext.getBeansOfType(PropertyResourceConfigurer.class);

  // 如果有定义,而且 applicationContext 类型合适,就进行实例化和调用
  if (!prcs.isEmpty() && applicationContext instanceof ConfigurableApplicationContext) {
    BeanDefinition mapperScannerBean = ((ConfigurableApplicationContext) applicationContext)
      .getBeanFactory().getBeanDefinition(beanName);

    // PropertyResourceConfigurer does not expose any methods to explicitly perform
    // property placeholder substitution. Instead, create a BeanFactory that just
    // contains this mapper scanner and post process the factory.
    DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
    factory.registerBeanDefinition(beanName, mapperScannerBean);

    for (PropertyResourceConfigurer prc : prcs.values()) {
      prc.postProcessBeanFactory(factory);
    }

    // 调用完,将配置项同步至这里的配置,这里使用了 beanName,但是感觉并不必须,可以直接设置,毕竟这里就
    // 可以直接拿到实例属性
    PropertyValues values = mapperScannerBean.getPropertyValues();

    this.basePackage = updatePropertyValue("basePackage", values);
    this.sqlSessionFactoryBeanName = updatePropertyValue("sqlSessionFactoryBeanName", values);
    this.sqlSessionTemplateBeanName = updatePropertyValue("sqlSessionTemplateBeanName", values);
  }
}

private String updatePropertyValue(String propertyName, PropertyValues values) {
  PropertyValue property = values.getPropertyValue(propertyName);

  if (property == null) {
    return null;
  }

  Object value = property.getValue();

  if (value == null) {
    return null;
  } else if (value instanceof String) {
    return value.toString();
  } else if (value instanceof TypedStringValue) {
    return ((TypedStringValue) value).getValue();
  } else {
    return null;
  }
}

到这里我们基本过完了包扫描前的准备工作,但是还有一个问题:为什么要专门加载一遍属性文件?我们先看一下PropertyResourceConfigurer的继承关系,方便猜。

7-基于Spring的框架-MyBatis——7-3 扩展及高级用法_第2张图片
2.png

情况基本明晰了:

之所以要配置记载资源并在这里执行,是因为我们扫描包并注册BD这里是使用的 BeanDefinitiopnRegistryPostProcessor

我们前面也介绍过这个接口相对它父类BeanFactoryPostProcessor的一个优点是可以进行 BD 的注册。相对的,ApplicationContext也保证这类型BD会在BeanFactoryPostProcessor操作之前完成所有的实例化、调用。

但是这里就出现了一个问题,我们在配置时很有可能会引用在.properties中配置的变量,但是如果我们先进行扫描,后执行PropertyResourceConfigurer的处理操作,就没法在扫描逻辑执行前对其依赖的配置属完成变量的替换。

所以我们先手动调用一下加载。

这里就涉及一些后处理器的东西了:我们在编写后处理器时,要尽量保证它是可重入的

通用包扫描逻辑

上面讲包扫描的操作交给了ClassPathMapperScanner,它是spring-mybatis定制的一个扫描类,因为Spring中的东西一般比较多,所以我们还是只关注核心逻辑,根据调用入口去看它的工作原理。

scanner.scan(StringUtils.tokenizeToStringArray(this.basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS));

其中常量如下:

String CONFIG_LOCATION_DELIMITERS = ",; \t\n";

这里将配置的包名数组进行合理的拆分。

public int scan(String... basePackages) {
  int beanCountAtScanStart = this.registry.getBeanDefinitionCount();
  this.doScan(basePackages);
  if (this.includeAnnotationConfig) {
    AnnotationConfigUtils.registerAnnotationConfigProcessors(this.registry);
  }

  return this.registry.getBeanDefinitionCount() - beanCountAtScanStart;
}

整体思路很清晰,记下现在有多少个注册的BD,然后扫描包并注册,后面计算一下新注册多少个并返回。这个思路和我们XmlBeanFactory中的解读很像。

只是这里多了一个步骤:AnnotationConfigUtils.registerAnnotationConfigProcessors(this.registry),它是判断在上下文中是否注册了一些基础类型的Bean(注释配置处理器),如果没有注册,就注册上。我们进行包扫描和BD注册,就需要对扫描的一些结果做出更多的处理,比如你的一些打标:@Configuration@Autowire@Required等等。这些标签的解读BD都是一些后处理器类型的,ApplicationContext后面会统一将他们挖掘出来,并进行注册

我们重点看包下类的的扫描操作:

protected Set doScan(String... basePackages) {
  Assert.notEmpty(basePackages, "At least one base package must be specified");
  Set beanDefinitions = new LinkedHashSet<>();
  for (String basePackage : basePackages) {
    // 找到这个包下面所有的类
    Set candidates = findCandidateComponents(basePackage);
    for (BeanDefinition candidate : candidates) {
      // TODO 计算BD的生命周期,并设置
      ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(candidate);
      candidate.setScope(scopeMetadata.getScopeName());
      // 生成Bean的id
      String beanName = this.beanNameGenerator.generateBeanName(candidate, this.registry);
      // 进行BD的一些属性的补全设置
      if (candidate instanceof AbstractBeanDefinition) {
        postProcessBeanDefinition((AbstractBeanDefinition) candidate, beanName);
      }
      if (candidate instanceof AnnotatedBeanDefinition) {
        AnnotationConfigUtils.processCommonDefinitionAnnotations((AnnotatedBeanDefinition) candidate);
      }
      // 看一下这个BD是否可以注册
      if (checkCandidate(beanName, candidate)) {
        BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(candidate, beanName);
        // 应用生命周期的设定
        definitionHolder =
          AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry);
        beanDefinitions.add(definitionHolder);
        // 注册
        registerBeanDefinition(definitionHolder, this.registry);
      }
    }
  }
  return beanDefinitions;
}

其中比较重要的有以下几个部分:

  1. findCandidateComponents(),扫描得到指定包下的可用BD
  2. checkCandidate(),检测获得的BD是否可以正常注册

剩下的基本在介绍Spring基础实施时介绍过了。

我们先看对包下类的扫描和生成BD:

public Set findCandidateComponents(String basePackage) {
  // TODO 这里不清楚是做什么的,看样子和资源加载差不多,
  // TODO 这两个应该是根据配置的资源加载方式的不同用了不同的API,不影响主逻辑,先看下面的吧
  if (this.componentsIndex != null && indexSupportsIncludeFilters()) {
    return addCandidateComponentsFromIndex(this.componentsIndex, basePackage);
  } else {
    return scanCandidateComponents(basePackage);
  }
}

// 这里很好理解,我们在编译打包后,所有的类就按照所属的包路径来放置了
private Set scanCandidateComponents(String basePackage) {
  Set candidates = new LinkedHashSet<>();
  try {
    // 拼接一下要加载的资源路径表达式, classpath*:包名/**/*.class
    String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX +
      resolveBasePackage(basePackage) + '/' + this.resourcePattern;
    // 获得路径下的所有资源列表,这里一个 Resource 就是一个 class 文件加载生成的
    Resource[] resources = getResourcePatternResolver().getResources(packageSearchPath);
    boolean traceEnabled = logger.isTraceEnabled();
    boolean debugEnabled = logger.isDebugEnabled();
    for (Resource resource : resources) {
      if (traceEnabled) {
        logger.trace("Scanning " + resource);
      }
      if (resource.isReadable()) { // 可读就进行读取、判断
        try {
          MetadataReader metadataReader = getMetadataReaderFactory().getMetadataReader(resource);
          // 判断此类是否合法【符合我们设置的类型过滤,且不在排除范围之内】
          if (isCandidateComponent(metadataReader)) {
            // 封装成BD
            ScannedGenericBeanDefinition sbd = new ScannedGenericBeanDefinition(metadataReader);
            sbd.setResource(resource);
            sbd.setSource(resource);
            // 此类是否可以注册BD
            // 1. 此类独立【它的实例化不需要依赖别的类】【不是那种非静态的内部类】
            // 2. 此类被标记为是具体的【不是接口/抽象类】或者此类虽然不具体,但是可以通过look-up通过Spring补全
            // 以上两点二选一,核心思想是这个类独立,可以进行实例化。
            if (isCandidateComponent(sbd)) {
              if (debugEnabled) {
                logger.debug("Identified candidate component class: " + resource);
              }
              candidates.add(sbd);
            } else {
              if (debugEnabled) {
                logger.debug("Ignored because not a concrete top-level class: " + resource);
              }
            }
          } else {
            if (traceEnabled) {
              logger.trace("Ignored because not matching any filter: " + resource);
            }
          }
        } catch (Throwable ex) {
          throw new BeanDefinitionStoreException(
            "Failed to read candidate component class: " + resource, ex);
        }
      } else {
        if (traceEnabled) {
          logger.trace("Ignored because not readable: " + resource);
        }
      }
    }
  } catch (IOException ex) {
    throw new BeanDefinitionStoreException("I/O failure during classpath scanning", ex);
  }
  return candidates;
}

看注释基本可以弄明白整个思路,有两个点需要明确:

  1. 我们在编译打包后,所有的类就按照所属的包路径来放置了,而不是我们的什么module之类的了。
  2. 我们扫描出的class究竟能不能打包成BD有以下限制
    1. 我们是否配置了允许——过滤器的限制
    2. 这个类自身是否允许——是否可独立生成实例

到这里我们拿到了包下的BD,接下来判断Spring是否允许BD的注册:

protected boolean checkCandidate(String beanName, BeanDefinition beanDefinition) throws IllegalStateException {
  if (!this.registry.containsBeanDefinition(beanName)) {
    return true;
  }
  BeanDefinition existingDef = this.registry.getBeanDefinition(beanName);
  BeanDefinition originatingDef = existingDef.getOriginatingBeanDefinition();
  if (originatingDef != null) {
    existingDef = originatingDef;
  }
  if (isCompatible(beanDefinition, existingDef)) {// 相融的,不用在继续重复覆盖了
    return false;
  }
  // 不相融,报错,同一个ID的Bean不能注册两次
  throw new ConflictingBeanDefinitionException("Annotation-specified bean name '" + beanName +
                                               "' for bean class [" + beanDefinition.getBeanClassName() + "] conflicts with existing, " +
                                               "non-compatible bean definition of same name and class [" + existingDef.getBeanClassName() + "]");
}

    // 判断我们扫描出来的BD和已经存在的BD是否是兼容的,情况如下
    // 1. 已经存在的BD是专门指定的
    // 2. 虽然旧BD也是扫描出来的,但是和我们扫出来的是出自一个源,也就是我们扫了两次
    // 3. 虽然源不一样,但是这两个BD一样,可能这中BD定制了一番处理,改了源
protected boolean isCompatible(BeanDefinition newDefinition, BeanDefinition existingDefinition) {
  return (!(existingDefinition instanceof ScannedGenericBeanDefinition) ||  // explicitly registered overriding bean
          (newDefinition.getSource() != null && newDefinition.getSource().equals(existingDefinition.getSource())) ||  // scanned same file twice
          newDefinition.equals(existingDefinition));  // scanned equivalent class twice
}

结合我们最开始介绍的BD注册的知识,这里不用赘述。

mybatis包扫描的定制

上面我们介绍了单纯的包扫描逻辑,基本能满足mybatis的需要,但是还有两个点没有满足:

  • 我们支持一些类排除的配置项目,我们需要将他们应用到我们的包扫描逻辑中
  • 扫描出的都是原本的接口,我们需要转化成MapperFactoryBean类型的BD

第一点在最开始委托给scanner类时做了设定:

public void registerFilters() {
  boolean acceptAllInterfaces = true;

  // if specified, use the given annotation and / or marker interface
  if (this.annotationClass != null) {
    addIncludeFilter(new AnnotationTypeFilter(this.annotationClass));
    acceptAllInterfaces = false;
  }

  // override AssignableTypeFilter to ignore matches on the actual marker interface
  // 如果你设定了必须要打了某种注解才允许构建BD,就按你的来
  if (this.markerInterface != null) {
    addIncludeFilter(new AssignableTypeFilter(this.markerInterface) {
      @Override
      protected boolean matchClassName(String className) {
        return false;
      }
    });
    acceptAllInterfaces = false;
  }

  // 否则默认所有的类都允许
  if (acceptAllInterfaces) {
    // default include filter that accepts all classes
    addIncludeFilter(new TypeFilter() {
      @Override
      public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) throws IOException {
        return true;
      }
    });
  }

  // exclude package-info.java
  // package-info.java 这个用的不多,知识一个包说明的类,没有任何逻辑
  addExcludeFilter(new TypeFilter() {
    @Override
    public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) throws IOException {
      String className = metadataReader.getClassMetadata().getClassName();
      return className.endsWith("package-info");
    }
  });
}

第二点它是直接扩展了doScan()方法:

public Set doScan(String... basePackages) {
  // 主干逻辑保持一致
  Set beanDefinitions = super.doScan(basePackages);

  if (beanDefinitions.isEmpty()) {
    logger.warn("No MyBatis mapper was found in '" + Arrays.toString(basePackages) + "' package. Please check your configuration.");
  } else {
    // 处理BD,修饰一下,因为java对对象的值传递,我们的修改可以影响到之前的注册
    processBeanDefinitions(beanDefinitions);
  }

  return beanDefinitions;
}

private void processBeanDefinitions(Set beanDefinitions) {
  GenericBeanDefinition definition;
  for (BeanDefinitionHolder holder : beanDefinitions) {
    definition = (GenericBeanDefinition) holder.getBeanDefinition();

    if (logger.isDebugEnabled()) {
      logger.debug("Creating MapperFactoryBean with name '" + holder.getBeanName() 
                   + "' and '" + definition.getBeanClassName() + "' mapperInterface");
    }

    // the mapper interface is the original class of the bean
    // but, the actual class of the bean is MapperFactoryBean
    definition.getConstructorArgumentValues().addGenericArgumentValue(definition.getBeanClassName()); // issue #59
    // 设置Class为MapperFactoryBean类型
    definition.setBeanClass(this.mapperFactoryBean.getClass());

    definition.getPropertyValues().add("addToConfig", this.addToConfig);

    boolean explicitFactoryUsed = false;
    // 剩下的参见MapperFactoryBean的基本用法吧
    if (StringUtils.hasText(this.sqlSessionFactoryBeanName)) {
      definition.getPropertyValues().add("sqlSessionFactory", new RuntimeBeanReference(this.sqlSessionFactoryBeanName));
      explicitFactoryUsed = true;
    } else if (this.sqlSessionFactory != null) {
      definition.getPropertyValues().add("sqlSessionFactory", this.sqlSessionFactory);
      explicitFactoryUsed = true;
    }

    if (StringUtils.hasText(this.sqlSessionTemplateBeanName)) {
      if (explicitFactoryUsed) {
        logger.warn("Cannot use both: sqlSessionTemplate and sqlSessionFactory together. sqlSessionFactory is ignored.");
      }
      definition.getPropertyValues().add("sqlSessionTemplate", new RuntimeBeanReference(this.sqlSessionTemplateBeanName));
      explicitFactoryUsed = true;
    } else if (this.sqlSessionTemplate != null) {
      if (explicitFactoryUsed) {
        logger.warn("Cannot use both: sqlSessionTemplate and sqlSessionFactory together. sqlSessionFactory is ignored.");
      }
      definition.getPropertyValues().add("sqlSessionTemplate", this.sqlSessionTemplate);
      explicitFactoryUsed = true;
    }

    if (!explicitFactoryUsed) {
      if (logger.isDebugEnabled()) {
        logger.debug("Enabling autowire by type for MapperFactoryBean with name '" + holder.getBeanName() + "'.");
      }
      definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
    }
  }
}

到这里,就完成指定包下类的扫描及mybatis的mapper的BD注册了。

扩展

mvc的包扫描也是基于我们的通用包扫描逻辑的,只是做了一些修饰而已。后面用到再说。

问题遗留

参考文献

你可能感兴趣的:(7-基于Spring的框架-MyBatis——7-3 扩展及高级用法)