概要
过度
前一篇文章介绍了Spring对MyBatis的一些接管,通过这种接管,简化了MyBatis配置的读取、SqlSession
的创建、Mapper的生成。使我们能像使用普通Bean一样使用包装好的数据库连接操作API。但是还存在着一个问题:
在生产环境中,尤其是复杂的业务场景,常常存在大量的表,或者要抽离出大量的数据库封装类,一个一个在Spring中注册Mapper太慢了。
我们在第一篇文章中介绍了一种方法,就是直接注册一个扫描器,他会帮助我们将制定包下面的类扫描,并完成BD的注册。本文主要介绍一下Spring的类扫描的基本实现。
内容简介
介绍MyBatis是如何扫描指定包下面的类并完成BD注册的。
所属环节
介绍MyBatis包扫描功能的实现,并给予此介绍Spring的包扫描功能。
上下环节
上文: 介绍Spring集成MyBatis时对基本API的封装
下文: 无
源码解析
入口
这里是我们启用Spring类扫描功能的配置项,它注册了一个特殊的Bean,并指明了要扫描的包。我们看一下这个类的继承关系,方便分析它的工作原理:
有三个方向:
- 实现了
ApplicationContextAware
,原因大概猜一下:获得上下文才能扫描到类后进行注册 - 实现了
BeanNameAware
,不清楚要拿到自己BD的id有什么目的,后面注意看看 - 实现了
InitializingBean
,这个初始化钩子可能要做一些基本的配置校验或者其他的逻辑 - 实现了
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 extends Annotation> 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
的继承关系,方便猜。
情况基本明晰了:
之所以要配置记载资源并在这里执行,是因为我们扫描包并注册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;
}
其中比较重要的有以下几个部分:
-
findCandidateComponents()
,扫描得到指定包下的可用BD -
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;
}
看注释基本可以弄明白整个思路,有两个点需要明确:
- 我们在编译打包后,所有的类就按照所属的包路径来放置了,而不是我们的什么module之类的了。
- 我们扫描出的class究竟能不能打包成BD有以下限制
- 我们是否配置了允许——过滤器的限制
- 这个类自身是否允许——是否可独立生成实例
到这里我们拿到了包下的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的包扫描也是基于我们的通用包扫描逻辑的,只是做了一些修饰而已。后面用到再说。