一、前言
一般我们会在datasource.xml中进行如下配置,但是其中每个配置项原理和用途是什么,并不是那么清楚,如果不清楚的话,在使用时候就很有可能会遇到坑,所以下面对这些配置项进行一一解说
(1)配置数据源
?xml version="1.0" encoding="UTF-8" standalone="no"?>
其中(1)是配置数据源,这里使用了druid连接池,用户可以根据自己的需要配置不同的数据源,也可以选择不适用数据库连接池,而直接使用具体的物理连接。
其中(2)创建sqlSessionFactory,用来在(3)时候使用。
其中(3)配置扫描器,扫描指定路径的mapper生成数据库操作代理类
二、SqlSessionFactory内幕
第二节配置中配置SqlSessionFactory的方式如下:
其中mapperLocations配置mapper.xml文件所在的路径,dataSource配置数据源,下面我们具体来看SqlSessionFactoryBean的代码,SqlSessionFactoryBean实现了FactoryBean和InitializingBean扩展接口,所以具有getObject和afterPropertiesSet方法(具体可以参考:https://gitbook.cn/gitchat/activity/5a84589a1f42d45a333f2a8e),下面我们从时序图具体看这两个方法内部做了什么:
如上时序图其中步骤(2)代码如下:
protected SqlSessionFactory buildSqlSessionFactory() throws IOException {
Configuration configuration;
XMLConfigBuilder xmlConfigBuilder = null;
...
//(3.1)
if (this.transactionFactory == null) {
this.transactionFactory = new SpringManagedTransactionFactory();
}
//(3.2)
configuration.setEnvironment(new Environment(this.environment, this.transactionFactory, this.dataSource));
//(3.3)
if (!isEmpty(this.mapperLocations)) {
for (Resource mapperLocation : this.mapperLocations) {
if (mapperLocation == null) {
continue;
}
try {
XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(mapperLocation.getInputStream(),
configuration, mapperLocation.toString(), configuration.getSqlFragments());
xmlMapperBuilder.parse();
} catch (Exception e) {
throw new NestedIOException("Failed to parse mapping resource: '" + mapperLocation + "'", e);
} finally {
ErrorContext.instance().reset();
}
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Parsed mapper file: '" + mapperLocation + "'");
}
}
} else {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Property 'mapperLocations' was not specified or no matching resources found");
}
}
//3.9
return this.sqlSessionFactoryBuilder.build(configuration);
}
如上代码(3.1)创建了一个Spring事务管理工厂,这个后面会用到。
代码(3.2)设置configuration对象的环境变量,其中dataSource为demo中配置文件中创建的数据源。
代码(3.3)中mapperLocations是一个数组,为demo中配置文件中配置的满足classpath:mapper/Mapper*.xml条件的mapper.xml文件,本demo会发现存在
[file[/Users/zhuizhumengxiang/workspace/mytool/distributtransaction/transactionconfig/transaction-demo/deep-learn-java/Start/target/classes/mapper/CourseDOMapper.xml]
,
file[/Users/zhuizhumengxiang/workspace/mytool/distributtransaction/transactionconfig/transaction-demo/deep-learn-java/Start/target/classes/mapper/UserDOMapper.xml]]
两个文件
代码(3.3)循环遍历每个mapper.xml,然后调用XMLMapperBuilder的parse方法进行解析。
XMLMapperBuilder的parse代码中configurationElement方法做具体解析,代码如下:
private void configurationElement(XNode context) {
try {
String namespace = context.getStringAttribute("namespace");
if (namespace == null || namespace.equals("")) {
throw new BuilderException("Mapper's namespace cannot be empty");
}
builderAssistant.setCurrentNamespace(namespace);
...
//(3.4)
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
//(3.5)
resultMapElements(context.evalNodes("/mapper/resultMap"));
//(3.6)
sqlElement(context.evalNodes("/mapper/sql"));
//(3.7)
buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
} catch (Exception e) {
throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
}
代码(3.4)解析mapper.xml中/mapper/parameterMap标签下内容,本demo中的XML文件中没有配置这个。
代码(3.5)解析mapper.xml中/mapper/resultMap标签下内容,然后存放到Configuration对象的resultMaps缓存里面,这里需要提一下,所有的mapper.xml文件共享一个Configuration对象,所有mapper.xml里面的resultMap都存放到同一个Configuration对象的resultMaps里面,其中key为mapper文件的namespace和resultMap的id组成,比如UserDoMapper.xml:
其中key为com.zlx.user.dal.sqlmap.CourseDOMapper.BaseResultMap,value则为存放一个map,map里面是column与property的映射。
- 代码(3.6)解析mapper.xml中/mapper/sql下的内容,然后保存到Configuration对象的sqlFragments缓存中,sqlFragments也是一个map,比如UserDoMapper.xml中的一个sql标签:
id, age
其中key为com.zlx.user.dal.sqlmap.CourseDOMapper.Base_Column_List,value作为一个记录sql标签内容的XNode节点。
- 代码(3.7)解析mapper.xml中select|insert|update|delete增删改查的语句,并封装为MappedStatement对象保存到Configuration的mappedStatements缓存中,mappedStatements也是一个map结构,比如:比如UserDoMapper.xml中的一个select标签:
其中key为com.zlx.user.dal.sqlmap.CourseDOMapper.selectByPrimaryKey,value为标签内封装为MappedStatement的对象。
至此configurationElement解析XML的步骤完毕了,下面我们看时序图中步骤(12)bindMapperForNamespace代码如下:
private void bindMapperForNamespace() {
String namespace = builderAssistant.getCurrentNamespace();
if (namespace != null) {
Class> boundType = null;
try {
boundType = Resources.classForName(namespace);
} catch (ClassNotFoundException e) {
//ignore, bound type is not required
}
if (boundType != null) {
if (!configuration.hasMapper(boundType)) {
//(3.8)
configuration.addLoadedResource("namespace:" + namespace);
configuration.addMapper(boundType);
}
}
}
}
其中代码(3.8)注册mapper接口的Class对象到configuration中的mapperRegistry管理的缓存knownMappers中,knownMappers是个map,其中key为具体mapper接口的Class对象,value为mapper接口的代理对象MapperProxyFactory。
注:SqlSessionFactoryBean作用之一是扫描配置的mapperLocations路径下的所有mapper.xml 文件,并对其进行解析,然后把解析的所有mapper文件的信息保存到一个全局的configuration对象的具体缓存中,然后注册每个mapper.xml对应的接口类到configuration中,并为每个接口类生成了一个代理bean.
然后时序图步骤15创建了一DefaultSqlSessionFactory对象,并且传递了上面全局的configuration对象。
步骤16则返回创建的DefaultSqlSessionFactory对象。
三、MapperScannerConfigurer内幕
第二节中MapperScannerConfigurer的配置方式如下:
其中sqlSessionFactory设置为第4节创建的DefaultSqlSessionFactory,basePackage为mapper接口类所在目录,annotationClass这是为注解@Resource,后面会知道标示只扫描basePackage路径下标注@Resource注解的mapper接口类。
MapperScannerConfigurer 实现了 BeanDefinitionRegistryPostProcessor, InitializingBean接口,所以会重写下面方法:
(5.1)
//在bean注册到ioc后创建实例前修改bean定义和新增bean注册,这个是在context的refresh方法被调用
void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException;
(5.2)
//set属性设置后被调用
void afterPropertiesSet() throws Exception;
更多关于Spring扩展接口的知识可以移步(https://gitbook.cn/gitchat/activity/5a84589a1f42d45a333f2a8e)
下面我们从时序图看这看postProcessBeanDefinitionRegistry和afterPropertiesSet扩展接口里面都做了些什么:
其中afterPropertiesSet代码如下:
public void afterPropertiesSet() throws Exception {
notNull(this.basePackage, "Property 'basePackage' is required");
}
可知是校验basePackage是否为null,为null会抛出异常。因为MapperScannerConfigurer作用就是扫描basePackage路径下的mapper接口类然后生成代理,所以不允许basePackage为null。
postProcessBeanDefinitionRegistry的代码如下:
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
if (this.processPropertyPlaceHolders) {
processPropertyPlaceHolders();
}
ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);
...
//5.3
scanner.setAnnotationClass(this.annotationClass);
//5.4
scanner.setSqlSessionFactory(this.sqlSessionFactory);
...
//5.5
scanner.registerFilters();
//5.6
scanner.scan(StringUtils.tokenizeToStringArray(this.basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS));
}
代码(5.3)设置注解类,这里设置的为@Resource注解,(5.4)设置sqlSessionFactory到ClassPathMapperScanner。
代码(5.5)根据设置的@Resource设置过滤器,代码如下:
public void registerFilters() {
boolean acceptAllInterfaces = true;
if (this.annotationClass != null) {
addIncludeFilter(new AnnotationTypeFilter(this.annotationClass));
acceptAllInterfaces = false;
}
...
}
public void addIncludeFilter(TypeFilter includeFilter) {
this.includeFilters.add(includeFilter);
}
可知具体是把@Resource注解作为了一个过滤器
- 代码(5.6)具体执行扫描,其中basePackage为我们设置的com.zlx.user.dal.sqlmap,basePackage设置的时候允许设置多个包路径并且使用
,; \t\n
进行分割,加上上面的过滤条件,就是说对basePackage路径下标注@Resource注解的mapper接口类进行代理。
具体执行扫描的是doScan方法,其代码如下:
protected Set doScan(String... basePackages) {
Assert.notEmpty(basePackages, "At least one base package must be specified");
Set beanDefinitions = new LinkedHashSet<>();
for (String basePackage : basePackages) {
//具体扫描符合条件的bean
Set candidates = findCandidateComponents(basePackage);
for (BeanDefinition candidate : candidates) {
...
if (checkCandidate(beanName, candidate)) {
BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(candidate, beanName);
definitionHolder =
AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry);
beanDefinitions.add(definitionHolder);
//注册到IOC容器
registerBeanDefinition(definitionHolder, this.registry);
}
}
}
return beanDefinitions;
}
如上代码可知是对每个包路径分别进行扫描,然后对符合条件的接口bean注册到IOC容器。
这里我们看下findCandidateComponents的逻辑:
private Set scanCandidateComponents(String basePackage) {
Set candidates = new LinkedHashSet<>();
try {
//5.8
String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX +
resolveBasePackage(basePackage) + '/' + this.resourcePattern;
Resource[] resources = getResourcePatternResolver().getResources(packageSearchPath);
...
//5.9
for (Resource resource : resources) {
if (traceEnabled) {
logger.trace("Scanning " + resource);
}
if (resource.isReadable()) {
try {
//5.10
MetadataReader metadataReader = getMetadataReaderFactory().getMetadataReader(resource);
if (isCandidateComponent(metadataReader)) {
ScannedGenericBeanDefinition sbd = new ScannedGenericBeanDefinition(metadataReader);
sbd.setResource(resource);
sbd.setSource(resource);
if (isCandidateComponent(sbd)) {
//5.11
candidates.add(sbd);
}
else {
}
}
...
}
...
}
...
}
}
...
return candidates;
}
如上代码其中(5.8)是根据我们设置的basePackage得到一个扫描路径,这里根据我们demo设置的值,拼接后packageSearchPath为classpath*:com/zlx/user/dal/sqlmap/**/*.class
,这里扫描出来的文件为:
file[/Users/zhuizhumengxiang/workspace/mytool/distributtransaction/transactionconfig/transaction-demo/deep-learn-java/Start/target/classes/com/zlx/user/dal/sqlmap/CourseDOMapper.class]
file[/Users/zhuizhumengxiang/workspace/mytool/distributtransaction/transactionconfig/transaction-demo/deep-learn-java/Start/target/classes/com/zlx/user/dal/sqlmap/CourseDOMapperNoAnnotition.class]
file[/Users/zhuizhumengxiang/workspace/mytool/distributtransaction/transactionconfig/transaction-demo/deep-learn-java/Start/target/classes/com/zlx/user/dal/sqlmap/UserDOMapper.class]
然后isCandidateComponent方法执行具体对上面扫描到的文件进行过滤,其代码:
protected boolean isCandidateComponent(MetadataReader metadataReader) throws IOException {
...
for (TypeFilter tf : this.includeFilters) {
if (tf.match(metadataReader, getMetadataReaderFactory())) {
return isConditionMatch(metadataReader);
}
}
return false;
}
上面我们讲解过添加了一个@Resource注解的过滤器,这里执行时候器match方法如下:
public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory)
throws IOException {
if (matchSelf(metadataReader)) {
return true;
}
...
return false;
}
//判断接口类是否有@Resource注解
protected boolean matchSelf(MetadataReader metadataReader) {
AnnotationMetadata metadata = metadataReader.getAnnotationMetadata();
return metadata.hasAnnotation(this.annotationType.getName()) ||
(this.considerMetaAnnotations && metadata.hasMetaAnnotation(this.annotationType.getName()));
}
经过过滤后CourseDOMapperNoAnnotition.class接口类被过滤了,因为其没有标注@Resource注解。只有CourseDOMapper和UserDOMapper两个标注@Resource的类注册到了IOC容器。
如上时序图注册后,还需要执行processBeanDefinitions对满足过滤条件的CourseDOMapper和UserDOMapper的bean定义进行修改,以便生成代理类,processBeanDefinitions代码如下:
private void processBeanDefinitions(Set beanDefinitions) {
GenericBeanDefinition definition;
for (BeanDefinitionHolder holder : beanDefinitions) {
definition = (GenericBeanDefinition) holder.getBeanDefinition();
// (5.12)
definition.getConstructorArgumentValues().addGenericArgumentValue(definition.getBeanClassName()); // issue #59
definition.setBeanClass(this.mapperFactoryBean.getClass());
...
//5.13
if (this.sqlSessionFactory != null) {
definition.getPropertyValues().add("sqlSessionFactory", this.sqlSessionFactory);
explicitFactoryUsed = true;
}
...
}
}
如上代码(5.12)修改bean定义的BeanClass为MapperFactoryBean,然后设置MapperFactoryBean的泛型构造函数参数为真正的被代理接口。也就是如果当前bean定义是com.zlx.user.dal.sqlmap.CourseDOMapper接口的,则设置当前bean定义的BeanClass为MapperFactoryBean,并设置com.zlx.user.dal.sqlmap.CourseDOMapper为MapperFactoryBean的构造函数参数。
代码(5.13)设置session工厂到bean定义。
注:MapperScannerConfigurer的作用是扫描指定路径下的Mapper接口类,并且可以制定过滤策略,然后对符合条件的bean定义进行修改以便在bean创建时候生成代理类,最终符合条件的mapper接口都会被转换为MapperFactoryBean,MapperFactoryBean中并且维护了第4节生成的DefaultSqlSessionFactory。
最后
更多本地事务咨询可以单击我
更多分布式事务咨询可以单击我
更多Spring事务配置解惑单击我
想了解更多关于粘包半包问题单击我
更多关于分布式系统中服务降级策略的知识可以单击 单击我
想系统学dubbo的单击我
想学并发的童鞋可以 单击我