Spring&Mybaits資料庫配置解惑
一、前言
一般我們會在datasource.xml中進行如下配置,但是其中每個配置項原理和用途是什麼,並不是那麼清楚,如果不清楚的話,在使用時候就很有可能會遇到坑,所以下面對這些配置項進行一一解說
(1)配置資料來源
?xml version="1.0" encoding="UTF-8" standalone="no"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:jee="http://www.springframework.org/schema/jee"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.0.xsd
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd
http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee-4.0.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.0.xsd">
<!-- (1) 資料來源 -->
<bean id="dataSource"
class="com.alibaba.druid.pool.DruidDataSource" init-method="init"
destroy-method="close">
<property name="driverClassName"
value="com.mysql.jdbc.Driver" />
<property name="url" value="jdbc:mysql://127.0.0.1:3306/test" />
<property name="username" value="root" />
<property name="password" value="123456" />
<property name="maxWait" value="3000" />
<property name="maxActive" value="28" />
<property name="initialSize" value="2" />
<property name="minIdle" value="0" />
<property name="timeBetweenEvictionRunsMillis" value="300000" />
<property name="testOnBorrow" value="false" />
<property name="testWhileIdle" value="true" />
<property name="validationQuery" value="select 1 from dual" />
<property name="filters" value="stat" />
</bean>
<!-- (2) session工廠 -->
<bean id="sqlSessionFactory"
class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="mapperLocations"
value="classpath*:mapper/*Mapper*.xml" />
<property name="dataSource" ref="dataSource" />
</bean>
<!-- (3) 配置掃描器,掃描指定路徑的mapper生成資料庫操作代理類 -->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="annotationClass"
value="javax.annotation.Resource"></property>
<property name="basePackage" value="com.zlx.user.dal.sqlmap" />
<property name="sqlSessionFactory" ref="sqlSessionFactory" />
</bean>
</beans>
其中(1)是配置資料來源,這裡使用了druid連線池,使用者可以根據自己的需要配置不同的資料來源,也可以選擇不適用資料庫連線池,而直接使用具體的物理連線。
其中(2)建立sqlSessionFactory,用來在(3)時候使用。
其中(3)配置掃描器,掃描指定路徑的mapper生成資料庫操作代理類
二、SqlSessionFactory內幕
第二節配置中配置SqlSessionFactory的方式如下:
<!-- (2) session工廠 -->
<bean id="sqlSessionFactory"
class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="mapperLocations"
value="classpath*:mapper/*Mapper*.xml" />
<property name="dataSource" ref="dataSource" />
</bean>
其中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:
<mapper namespace="com.zlx.user.dal.sqlmap.UserDOMapper" >
<resultMap id="BaseResultMap" type="com.zlx.user.dal.dao.UserDO" >
<id column="id" property="id" jdbcType="INTEGER" />
<result column="age" property="age" jdbcType="INTEGER" />
</resultMap>
其中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標籤:
<sql id="Base_Column_List" >
id, age
</sql>
其中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標籤:
<select id="selectByPrimaryKey" resultMap="BaseResultMap" parameterType="java.lang.Integer" >
select
<include refid="Base_Column_List" />
from user
where id = #{id,jdbcType=INTEGER}
</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的配置方式如下:
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="annotationClass"
value="javax.annotation.Resource"></property>
<property name="basePackage" value="com.zlx.user.dal.sqlmap" />
<property name="sqlSessionFactory" ref="sqlSessionFactory" />
</bean>
其中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<BeanDefinitionHolder> doScan(String... basePackages) {
Assert.notEmpty(basePackages, "At least one base package must be specified");
Set<BeanDefinitionHolder> beanDefinitions = new LinkedHashSet<>();
for (String basePackage : basePackages) {
//具體掃描符合條件的bean
Set<BeanDefinition> 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<BeanDefinition> scanCandidateComponents(String basePackage) {
Set<BeanDefinition> 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<BeanDefinitionHolder> 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的單擊我
想學併發的童鞋可以 單擊我
相關文章
- 【DBA | IT人生】資料庫解惑系列資料庫
- 資料庫配置資料庫
- Serverless 解惑——函式計算如何訪問 Mongo 資料庫Server函式Go資料庫
- Serverless 解惑——函式計算如何訪問 Redis 資料庫Server函式Redis資料庫
- Serverless 解惑——函式計算如何訪問 MySQL 資料庫Server函式MySql資料庫
- Serverless 解惑——函式計算如何訪問 PostgreSQL 資料庫Server函式SQL資料庫
- 織夢資料庫配置檔案修改資料庫配置方法資料庫
- Jira資料庫配置資料庫
- jive資料庫配置資料庫
- 配置session——資料庫Session資料庫
- Oracle資料庫配置Oracle資料庫
- Serverless 解惑——函式計算如何訪問 SQL Server 資料庫Server函式SQL資料庫
- django 配置mysql資料庫DjangoMySql資料庫
- luffy之資料庫配置資料庫
- django配置mysql資料庫DjangoMySql資料庫
- 資料庫配置檢查資料庫
- 2 建立和配置資料庫資料庫
- 4 為效能配置資料庫資料庫
- mysql主從資料庫配置MySql資料庫
- Mybatis配置資料庫連線MyBatis資料庫
- ES資料庫高可用配置資料庫
- Vapor如何配置MySQL資料庫VaporMySql資料庫
- docker配置前端和資料庫Docker前端資料庫
- 一、配置etcd資料庫資料庫
- MySQL資料庫時區配置MySql資料庫
- MySQL 配置資料庫編碼MySql資料庫
- 重新配置OpenFire資料庫資料庫
- solr連線資料庫配置Solr資料庫
- 為RMAN操作配置資料庫資料庫
- 配置資料庫問題?求救資料庫
- Oracle配置資料庫診斷Oracle資料庫
- 資料庫查詢配置值資料庫
- SQL Server 資料庫同步配置SQLServer資料庫
- MyBatis-04-資料庫配置MyBatis資料庫
- django setting 配置資料庫Django資料庫
- SequoiaDB資料庫之資料庫的配置與啟動資料庫
- 織夢資料庫配置檔案資料庫損壞:嘗試修復資料庫資料庫
- PbootCMS資料庫配置,修改為Mysql資料庫,配置Mysql出錯解決辦法boot資料庫MySql